From 7a1bf4668627e172501ce38d6739f7d042dad045 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 12:36:51 +0000 Subject: [PATCH 001/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 7882d0c10..13cfd8e18 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=4309751727f69b3c7878be2156e5e0f7d7d838c5 -short_hash=4309751 -message=避免不必要的重试 -date=2026-01-09 21:28:50 +0800 +full_hash=71b786c260a6324a02170705c18245037e25849e +short_hash=71b786c +message=Merge pull request #263 from su-kaka/dev +date=2026-01-10 20:36:42 +0800 From b2f5e57f2d8abc8cb77f03dcdb296d7028aa9c31 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 10 Jan 2026 20:40:47 +0800 Subject: [PATCH 002/211] Update base_router.py --- src/router/base_router.py | 257 +------------------------------------- 1 file changed, 1 insertion(+), 256 deletions(-) diff --git a/src/router/base_router.py b/src/router/base_router.py index f09111711..e17b64818 100644 --- a/src/router/base_router.py +++ b/src/router/base_router.py @@ -4,42 +4,8 @@ """ from typing import List, Optional -from fastapi import Response from src.models import Model, ModelList -from log import log -import json - - -# ==================== 错误响应 ==================== - -def create_error_response( - message: str, - status_code: int = 500, - error_type: str = "api_error" -) -> Response: - """ - 创建标准化的错误响应 - - Args: - message: 错误消息 - status_code: HTTP状态码 - error_type: 错误类型 - - Returns: - FastAPI Response对象 - """ - return Response( - content=json.dumps({ - "error": { - "message": message, - "type": error_type, - "code": status_code - } - }), - status_code=status_code, - media_type="application/json", - ) # ==================== 模型列表处理 ==================== @@ -137,225 +103,4 @@ def create_gemini_model_list( } gemini_models.append(model_info) - return {"models": gemini_models} - - -# ==================== 流式响应管理 ==================== - -async def cleanup_stream_resources(stream_ctx, client) -> None: - """ - 清理流式响应资源 - - Args: - stream_ctx: 流上下文 - client: HTTP客户端 - """ - # 按正确顺序清理资源:先关闭stream,再关闭client - if stream_ctx: - try: - await stream_ctx.__aexit__(None, None, None) - except Exception as e: - log.debug(f"Error cleaning up stream_ctx: {e}") - - if client: - try: - await client.aclose() - except Exception as e: - log.debug(f"Error closing client: {e}") - - -async def create_error_stream( - error_message: str, - status_code: int = 500, - format: str = "openai" -) -> bytes: - """ - 创建错误流数据 - - Args: - error_message: 错误消息 - status_code: HTTP状态码 - format: 响应格式("openai" 或 "gemini") - - Returns: - SSE格式的错误数据 - """ - if format == "openai": - error_data = { - "error": { - "message": error_message, - "type": "api_error", - "code": status_code, - } - } - else: # gemini - error_data = { - "error": { - "message": error_message, - "code": status_code, - "status": "INTERNAL" - } - } - - return f"data: {json.dumps(error_data)}\n\n".encode() - - -# ==================== 模型名称处理 ==================== - -def extract_base_model_name(model_path: str) -> str: - """ - 从路径格式的模型名中提取基础模型名 - - Args: - model_path: 可能包含 "models/" 前缀的模型名 - - Returns: - 基础模型名 - """ - # 去掉 "models/" 前缀 - if model_path.startswith("models/"): - return model_path[7:] - return model_path - - -# ==================== 日志辅助 ==================== - -def log_request_info( - mode: str, - model: str, - credential_name: str, - is_streaming: bool -) -> None: - """ - 记录请求信息 - - Args: - mode: 模式(如 "ANTIGRAVITY", "GEMINICLI") - model: 模型名称 - credential_name: 凭证名称 - is_streaming: 是否是流式请求 - """ - request_type = "streaming" if is_streaming else "non-streaming" - log.info( - f"[{mode}] {request_type.capitalize()} request for model: {model}, " - f"using credential: {credential_name}" - ) - - -# ==================== 统一流式包装器 ==================== - -from typing import AsyncGenerator, Tuple, Any, Callable, Awaitable -from fastapi.responses import StreamingResponse - - -def wrap_stream_with_cleanup( - filtered_lines: AsyncGenerator, - stream_ctx: Any, - client: Any -) -> AsyncGenerator: - """ - 包装流式响应,自动清理资源 - - 这个函数是所有流式响应的统一包装器,确保在流结束时正确清理资源。 - 适用于 antigravity 和 geminicli 的所有流式 API。 - - Args: - filtered_lines: 原始行生成器 - stream_ctx: 流上下文管理器 - client: HTTP 客户端 - - Returns: - 带资源清理的行生成器 - - 示例: - ```python - resources, _, _ = await send_xxx_request_stream(payload, cred_mgr) - filtered_lines, stream_ctx, client = resources - - return StreamingResponse( - wrap_stream_with_cleanup(filtered_lines, stream_ctx, client), - media_type="text/event-stream" - ) - ``` - """ - async def line_generator(): - try: - async for line in filtered_lines: - yield line - finally: - await cleanup_stream_resources(stream_ctx, client) - - return line_generator() - - -async def wrap_stream_with_processor( - filtered_lines: AsyncGenerator, - stream_ctx: Any, - client: Any, - processor: Callable[[Any], Awaitable[Any]] -) -> AsyncGenerator: - """ - 包装流式响应,应用处理器并清理资源 - - 这个函数用于需要对流式数据进行转换处理的场景(如 Anthropic SSE 转换)。 - - Args: - filtered_lines: 原始行生成器 - stream_ctx: 流上下文管理器 - client: HTTP 客户端 - processor: 异步处理器函数,接收原始流并产出处理后的数据 - - Returns: - 带处理和资源清理的生成器 - - 示例: - ```python - resources, cred_name, _ = await send_xxx_request_stream(payload, cred_mgr) - filtered_lines, stream_ctx, client = resources - - return StreamingResponse( - wrap_stream_with_processor( - filtered_lines, stream_ctx, client, - lambda lines: gemini_sse_to_anthropic_sse( - lines, model=model, message_id=msg_id, ... - ) - ), - media_type="text/event-stream" - ) - ``` - """ - async def processed_generator(): - try: - async for chunk in processor(filtered_lines): - yield chunk - finally: - await cleanup_stream_resources(stream_ctx, client) - - return processed_generator() - - -def create_streaming_response_from_resources( - resources: Tuple[AsyncGenerator, Any, Any], - media_type: str = "text/event-stream" -) -> StreamingResponse: - """ - 从流式资源创建 StreamingResponse(最简单的包装) - - Args: - resources: (filtered_lines, stream_ctx, client) 元组 - media_type: 响应的媒体类型 - - Returns: - StreamingResponse 对象 - - 示例: - ```python - resources, _, _ = await send_xxx_request_stream(payload, cred_mgr) - return create_streaming_response_from_resources(resources) - ``` - """ - filtered_lines, stream_ctx, client = resources - return StreamingResponse( - wrap_stream_with_cleanup(filtered_lines, stream_ctx, client), - media_type=media_type - ) + return {"models": gemini_models} \ No newline at end of file From ad072f46f6a084849054b25894d23ab7809278d1 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 10 Jan 2026 20:56:31 +0800 Subject: [PATCH 003/211] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/antigravity/model_list.py | 75 +++++++++++++-- src/router/geminicli/model_list.py | 28 +++++- src/utils.py | 137 +++++++++++---------------- 3 files changed, 142 insertions(+), 98 deletions(-) diff --git a/src/router/antigravity/model_list.py b/src/router/antigravity/model_list.py index fb6ee1e6e..f27c81822 100644 --- a/src/router/antigravity/model_list.py +++ b/src/router/antigravity/model_list.py @@ -17,13 +17,17 @@ # 本地模块 - 工具和认证 from src.utils import ( - get_available_models, get_base_model_from_feature_model, - authenticate_gemini_flexible + authenticate_flexible ) +# 本地模块 - API +from src.api.antigravity import fetch_available_models + # 本地模块 - 基础路由工具 -from src.router.base_router import create_gemini_model_list +from src.router.base_router import create_gemini_model_list, create_openai_model_list +from src.models import model_to_dict +from log import log # ==================== 路由器初始化 ==================== @@ -31,18 +35,71 @@ router = APIRouter() +# ==================== 辅助函数 ==================== + +async def get_antigravity_models_with_features(): + """ + 获取 Antigravity 模型列表并添加功能前缀 + + Returns: + 带有功能前缀的模型列表 + """ + # 从 API 获取基础模型列表 + base_models_data = await fetch_available_models() + + if not base_models_data: + log.warning("[ANTIGRAVITY MODEL LIST] 无法获取模型列表,返回空列表") + return [] + + # 提取模型 ID + base_model_ids = [model['id'] for model in base_models_data if 'id' in model] + + # 添加功能前缀 + models = [] + for base_model in base_model_ids: + # 基础模型 + models.append(base_model) + + # 假流式模型 (前缀格式) + models.append(f"假流式/{base_model}") + + # 流式抗截断模型 (仅在流式传输时有效,前缀格式) + models.append(f"流式抗截断/{base_model}") + + log.info(f"[ANTIGRAVITY MODEL LIST] 生成了 {len(models)} 个模型(包含功能前缀)") + return models + + # ==================== API 路由 ==================== @router.get("/antigravity/v1beta/models") -@router.get("/antigravity/v1/models") -async def list_gemini_models(token: str = Depends(authenticate_gemini_flexible)): +async def list_gemini_models(token: str = Depends(authenticate_flexible)): """ - 返回Gemini格式的模型列表 - - 使用 create_gemini_model_list 工具函数创建标准格式 + 返回 Gemini 格式的模型列表 + + 从 src.api.antigravity.fetch_available_models 动态获取模型列表 + 并添加假流式和流式抗截断前缀 """ - models = get_available_models("antigravity") + models = await get_antigravity_models_with_features() + log.info("[ANTIGRAVITY MODEL LIST] 返回 Gemini 格式") return JSONResponse(content=create_gemini_model_list( models, base_name_extractor=get_base_model_from_feature_model )) + + +@router.get("/antigravity/v1/models") +async def list_openai_models(token: str = Depends(authenticate_flexible)): + """ + 返回 OpenAI 格式的模型列表 + + 从 src.api.antigravity.fetch_available_models 动态获取模型列表 + 并添加假流式和流式抗截断前缀 + """ + models = await get_antigravity_models_with_features() + log.info("[ANTIGRAVITY MODEL LIST] 返回 OpenAI 格式") + model_list = create_openai_model_list(models, owned_by="google") + return JSONResponse(content={ + "object": "list", + "data": [model_to_dict(model) for model in model_list.data] + }) diff --git a/src/router/geminicli/model_list.py b/src/router/geminicli/model_list.py index 503b8b9fb..333a987a8 100644 --- a/src/router/geminicli/model_list.py +++ b/src/router/geminicli/model_list.py @@ -19,11 +19,13 @@ from src.utils import ( get_available_models, get_base_model_from_feature_model, - authenticate_gemini_flexible + authenticate_flexible ) # 本地模块 - 基础路由工具 -from src.router.base_router import create_gemini_model_list +from src.router.base_router import create_gemini_model_list, create_openai_model_list +from src.models import model_to_dict +from log import log # ==================== 路由器初始化 ==================== @@ -34,15 +36,31 @@ # ==================== API 路由 ==================== @router.get("/v1beta/models") -@router.get("/v1/models") -async def list_gemini_models(token: str = Depends(authenticate_gemini_flexible)): +async def list_gemini_models(token: str = Depends(authenticate_flexible)): """ - 返回Gemini格式的模型列表 + 返回 Gemini 格式的模型列表 使用 create_gemini_model_list 工具函数创建标准格式 """ models = get_available_models("gemini") + log.info("[GEMINICLI MODEL LIST] 返回 Gemini 格式") return JSONResponse(content=create_gemini_model_list( models, base_name_extractor=get_base_model_from_feature_model )) + + +@router.get("/v1/models") +async def list_openai_models(token: str = Depends(authenticate_flexible)): + """ + 返回 OpenAI 格式的模型列表 + + 使用 create_openai_model_list 工具函数创建标准格式 + """ + models = get_available_models("gemini") + log.info("[GEMINICLI MODEL LIST] 返回 OpenAI 格式") + model_list = create_openai_model_list(models, owned_by="google") + return JSONResponse(content={ + "object": "list", + "data": [model_to_dict(model) for model in model_list.data] + }) diff --git a/src/utils.py b/src/utils.py index 2bdd7d20a..83ca21dd9 100644 --- a/src/utils.py +++ b/src/utils.py @@ -137,135 +137,104 @@ def get_available_models(router_type: str = "openai") -> List[str]: # ====================== Authentication Functions ====================== -async def authenticate_bearer( +async def authenticate_flexible( + request: Request, authorization: Optional[str] = Header(None), x_api_key: Optional[str] = Header(None, alias="x-api-key"), - access_token: Optional[str] = Header(None, alias="access_token") + access_token: Optional[str] = Header(None, alias="access_token"), + x_goog_api_key: Optional[str] = Header(None, alias="x-goog-api-key"), + key: Optional[str] = Query(None) ) -> str: """ - Bearer Token 认证 - + 统一的灵活认证函数,支持多种认证方式 + 此函数可以直接用作 FastAPI 的 Depends 依赖 - - 支持的认证字段: - - authorization (Bearer token) - - x-api-key - - access_token - + + 支持的认证方式: + - URL 参数: key + - HTTP 头部: Authorization (Bearer token) + - HTTP 头部: x-api-key + - HTTP 头部: access_token + - HTTP 头部: x-goog-api-key + Args: + request: FastAPI Request 对象 authorization: Authorization 头部值(自动注入) x_api_key: x-api-key 头部值(自动注入) access_token: access_token 头部值(自动注入) - + x_goog_api_key: x-goog-api-key 头部值(自动注入) + key: URL 参数 key(自动注入) + Returns: 验证通过的token - + Raises: - HTTPException: 认证失败时抛出401或403异常 - + HTTPException: 认证失败时抛出异常 + 使用示例: @router.post("/endpoint") - async def endpoint(token: str = Depends(authenticate_bearer)): + async def endpoint(token: str = Depends(authenticate_flexible)): # token 已验证通过 pass """ - password = await get_api_password() token = None - - # 1. 尝试从 x-api-key 获取 - if x_api_key: + auth_method = None + + # 1. 尝试从 URL 参数 key 获取(Google 官方标准方式) + if key: + token = key + auth_method = "URL parameter 'key'" + + # 2. 尝试从 x-goog-api-key 头部获取(Google API 标准方式) + elif x_goog_api_key: + token = x_goog_api_key + auth_method = "x-goog-api-key header" + + # 3. 尝试从 x-api-key 头部获取 + elif x_api_key: token = x_api_key - - # 2. 尝试从 access_token 获取 + auth_method = "x-api-key header" + + # 4. 尝试从 access_token 头部获取 elif access_token: token = access_token - - # 3. 尝试从 authorization 获取 + auth_method = "access_token header" + + # 5. 尝试从 Authorization 头部获取 elif authorization: - # 检查是否是 Bearer token if not authorization.startswith("Bearer "): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication scheme. Use 'Bearer '", headers={"WWW-Authenticate": "Bearer"}, ) - # 提取 token token = authorization[7:] # 移除 "Bearer " 前缀 - + auth_method = "Authorization Bearer header" + # 检查是否提供了任何认证凭据 if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Missing authentication credentials. Use 'Authorization: Bearer ', 'x-api-key: ', or 'access_token: '", + detail="Missing authentication credentials. Use 'key' URL parameter, 'x-goog-api-key', 'x-api-key', 'access_token' header, or 'Authorization: Bearer '", headers={"WWW-Authenticate": "Bearer"}, ) - + # 验证 token if token != password: + log.error(f"Authentication failed using {auth_method}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="密码错误" ) - + + log.debug(f"Authentication successful using {auth_method}") return token -async def authenticate_gemini_flexible( - request: Request, - x_goog_api_key: Optional[str] = Header(None, alias="x-goog-api-key"), - key: Optional[str] = Query(None) -) -> str: - """ - Gemini 灵活认证:支持 x-goog-api-key 头部、URL 参数 key 或 Authorization Bearer - - 此函数可以直接用作 FastAPI 的 Depends 依赖 - - Args: - request: FastAPI Request 对象 - x_goog_api_key: x-goog-api-key 头部值(自动注入) - key: URL 参数 key(自动注入) - - Returns: - 验证通过的API密钥 - - Raises: - HTTPException: 认证失败时抛出400异常 - - 使用示例: - @router.post("/endpoint") - async def endpoint(api_key: str = Depends(authenticate_gemini_flexible)): - # api_key 已验证通过 - pass - """ - - password = await get_api_password() - - # 尝试从URL参数key获取(Google官方标准方式) - if key: - log.debug("Using URL parameter key authentication") - if key == password: - return key - - # 尝试从Authorization头获取(兼容旧方式) - auth_header = request.headers.get("authorization") - if auth_header and auth_header.startswith("Bearer "): - token = auth_header[7:] # 移除 "Bearer " 前缀 - log.debug("Using Bearer token authentication") - if token == password: - return token - - # 尝试从x-goog-api-key头获取(新标准方式) - if x_goog_api_key: - log.debug("Using x-goog-api-key authentication") - if x_goog_api_key == password: - return x_goog_api_key - - log.error(f"Authentication failed. Headers: {dict(request.headers)}, Query params: key={key}") - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Missing or invalid authentication. Use 'key' URL parameter, 'x-goog-api-key' header, or 'Authorization: Bearer '", - ) +# 为了保持向后兼容,保留旧函数名作为别名 +authenticate_bearer = authenticate_flexible +authenticate_gemini_flexible = authenticate_flexible # ====================== Panel Authentication Functions ====================== From c3ca9cb6b50c31b34e4e6ce8ba74e6dcb8995626 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 12:56:37 +0000 Subject: [PATCH 004/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 13cfd8e18..b0aa1e194 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=71b786c260a6324a02170705c18245037e25849e -short_hash=71b786c -message=Merge pull request #263 from su-kaka/dev -date=2026-01-10 20:36:42 +0800 +full_hash=ad072f46f6a084849054b25894d23ab7809278d1 +short_hash=ad072f4 +message=修复模型列表 +date=2026-01-10 20:56:31 +0800 From fd9db5d7c56f0226cdfe09ab3688ccb7e085a4fe Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 10 Jan 2026 21:21:55 +0800 Subject: [PATCH 005/211] =?UTF-8?q?=E5=8E=BB=E6=8E=89=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 45 +------------------------------ src/converter/gemini_fix.py | 33 ----------------------- src/converter/openai2gemini.py | 28 ++++--------------- 3 files changed, 6 insertions(+), 100 deletions(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index ab77b1f51..7244e9fce 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -11,7 +11,6 @@ from typing import Any, AsyncIterator, Dict, List, Optional, Union from log import log -from src.converter.gemini_fix import build_system_instruction_from_list from src.converter.utils import merge_system_messages from src.converter.thoughtSignature_fix import ( @@ -421,38 +420,6 @@ def reorganize_tool_messages(contents: List[Dict[str, Any]]) -> List[Dict[str, A return new_contents -# ============================================================================ -# 6. System Instruction 构建 -# ============================================================================ - -def build_system_instruction(system: Any) -> Optional[Dict[str, Any]]: - """ - 将 Anthropic system 字段转换为下游 systemInstruction - - 统一使用 gemini_fix.build_system_instruction_from_list 来处理 - """ - if not system: - return None - - system_instructions: List[str] = [] - - if isinstance(system, str): - if _is_non_whitespace_text(system): - system_instructions.append(str(system)) - elif isinstance(system, list): - for item in system: - if isinstance(item, dict) and item.get("type") == "text": - text = item.get("text", "") - if _is_non_whitespace_text(text): - system_instructions.append(str(text)) - else: - if _is_non_whitespace_text(system): - system_instructions.append(str(system)) - - # 使用统一的函数构建 systemInstruction - return build_system_instruction_from_list(system_instructions) - - # ============================================================================ # 7. Generation Config 构建 # ============================================================================ @@ -600,13 +567,6 @@ async def anthropic_to_gemini_request(payload: Dict[str, Any]) -> Dict[str, Any] contents = convert_messages_to_contents(messages, include_thinking=should_include_thinking) contents = reorganize_tool_messages(contents) - # 转换系统指令 - system_instruction = build_system_instruction(payload.get("system")) - - # 如果merge_system_messages已经添加了systemInstruction,优先使用它 - if "systemInstruction" in payload and not system_instruction: - system_instruction = payload["systemInstruction"] - # 转换工具 tools = convert_tools(payload.get("tools")) @@ -615,10 +575,7 @@ async def anthropic_to_gemini_request(payload: Dict[str, Any]) -> Dict[str, Any] "contents": contents, "generationConfig": generation_config, } - - if system_instruction: - gemini_request["systemInstruction"] = system_instruction - + if tools: gemini_request["tools"] = tools diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index ca3d5850a..8546fe1d6 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -23,39 +23,6 @@ } -def build_system_instruction_from_list(system_instructions: List[str]) -> Optional[Dict[str, Any]]: - """ - 从字符串列表构建 Gemini systemInstruction 对象 - - Args: - system_instructions: 系统指令字符串列表 - - Returns: - Gemini 格式的 systemInstruction 字典,如果列表为空则返回 None - - Example: - >>> build_system_instruction_from_list(["You are helpful.", "Be concise."]) - { - "parts": [ - {"text": "You are helpful."}, - {"text": "Be concise."} - ] - } - """ - if not system_instructions: - return None - - parts = [] - for instruction in system_instructions: - if instruction and instruction.strip(): - parts.append({"text": instruction}) - - if not parts: - return None - - return {"parts": parts} - - def clean_tools_for_gemini(tools: Optional[List[Dict[str, Any]]]) -> Optional[List[Dict[str, Any]]]: """ diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 65dfd9dfe..3652c2e04 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -475,14 +475,10 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di openai_request = await merge_system_messages(openai_request) contents = [] - system_instructions = [] # 提取消息列表 messages = openai_request.get("messages", []) - # 第一阶段:收集连续的system消息 - collecting_system = True - for message in messages: role = message.get("role", "user") content = message.get("content", "") @@ -524,21 +520,9 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di }) continue - # 处理系统消息 + # system 消息已经由 merge_system_messages 处理,这里跳过 if role == "system": - if collecting_system: - if isinstance(content, str): - system_instructions.append(content) - elif isinstance(content, list): - for part in content: - if part.get("type") == "text" and part.get("text"): - system_instructions.append(part["text"]) - continue - else: - # 后续的system消息转换为user消息 - role = "user" - else: - collecting_system = False + continue # 将OpenAI角色映射到Gemini角色 if role == "assistant": @@ -636,11 +620,9 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di "generationConfig": generation_config } - # 添加系统指令 - if system_instructions: - gemini_request["systemInstruction"] = { - "parts": [{"text": "\n\n".join(system_instructions)}] - } + # 如果 merge_system_messages 已经添加了 systemInstruction,使用它 + if "systemInstruction" in openai_request: + gemini_request["systemInstruction"] = openai_request["systemInstruction"] # 处理工具 if "tools" in openai_request and openai_request["tools"]: From 03b122466ff188e5dbb734749ca662a13b328978 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 10 Jan 2026 21:32:47 +0800 Subject: [PATCH 006/211] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dclaude=E7=9A=84system?= =?UTF-8?q?=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index 7244e9fce..f39b83ad8 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -257,6 +257,11 @@ def convert_messages_to_contents( for msg in messages: role = msg.get("role", "user") + + # system 消息已经由 merge_system_messages 处理,这里跳过 + if role == "system": + continue + gemini_role = "model" if role == "assistant" else "user" raw_content = msg.get("content", "") @@ -576,6 +581,10 @@ async def anthropic_to_gemini_request(payload: Dict[str, Any]) -> Dict[str, Any] "generationConfig": generation_config, } + # 如果 merge_system_messages 已经添加了 systemInstruction,使用它 + if "systemInstruction" in payload: + gemini_request["systemInstruction"] = payload["systemInstruction"] + if tools: gemini_request["tools"] = tools From 4c1e33f9c991b7925999f5e29cc39c3f612cf56b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 13:32:58 +0000 Subject: [PATCH 007/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index b0aa1e194..de54dda36 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=ad072f46f6a084849054b25894d23ab7809278d1 -short_hash=ad072f4 -message=修复模型列表 -date=2026-01-10 20:56:31 +0800 +full_hash=75b84589ad4f33ced012ab4f9b5f6e899298f13d +short_hash=75b8458 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-10 21:32:49 +0800 From 3acce960a424cbe46b3615a6a1984bee4db10a6f Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 10 Jan 2026 22:39:04 +0800 Subject: [PATCH 008/211] =?UTF-8?q?1024=E7=9A=84=E6=80=9D=E8=80=83?= =?UTF-8?q?=E9=A2=84=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 113 +++--------------------------- src/converter/gemini_fix.py | 8 +-- 2 files changed, 13 insertions(+), 108 deletions(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index f39b83ad8..94c3a0966 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -8,7 +8,7 @@ import json import os import uuid -from typing import Any, AsyncIterator, Dict, List, Optional, Union +from typing import Any, AsyncIterator, Dict, List, Optional from log import log from src.converter.utils import merge_system_messages @@ -18,7 +18,6 @@ decode_tool_id_and_signature ) -DEFAULT_THINKING_BUDGET = 1024 DEFAULT_TEMPERATURE = 0.4 _DEBUG_TRUE = {"1", "true", "yes", "on"} @@ -75,35 +74,7 @@ def _remove_nulls_for_tool_input(value: Any) -> Any: return value # ============================================================================ -# 2. Thinking 配置 -# ============================================================================ - -def get_thinking_config(thinking: Optional[Union[bool, Dict[str, Any]]]) -> Dict[str, Any]: - """ - 根据 Anthropic/Claude 请求的 thinking 参数生成下游 thinkingConfig。 - """ - if thinking is None: - return {"includeThoughts": True, "thinkingBudget": DEFAULT_THINKING_BUDGET} - - if isinstance(thinking, bool): - if thinking: - return {"includeThoughts": True, "thinkingBudget": DEFAULT_THINKING_BUDGET} - return {"includeThoughts": False} - - if isinstance(thinking, dict): - thinking_type = thinking.get("type", "enabled") - is_enabled = thinking_type == "enabled" - if not is_enabled: - return {"includeThoughts": False} - - budget = thinking.get("budget_tokens", DEFAULT_THINKING_BUDGET) - return {"includeThoughts": True, "thinkingBudget": budget} - - return {"includeThoughts": True, "thinkingBudget": DEFAULT_THINKING_BUDGET} - - -# ============================================================================ -# 3. JSON Schema 清理 +# 2. JSON Schema 清理 # ============================================================================ def clean_json_schema(schema: Any) -> Any: @@ -429,12 +400,12 @@ def reorganize_tool_messages(contents: List[Dict[str, Any]]) -> List[Dict[str, A # 7. Generation Config 构建 # ============================================================================ -def build_generation_config(payload: Dict[str, Any]) -> tuple[Dict[str, Any], bool]: +def build_generation_config(payload: Dict[str, Any]) -> Dict[str, Any]: """ 根据 Anthropic Messages 请求构造下游 generationConfig。 Returns: - (generation_config, should_include_thinking): 元组 + generation_config: 生成配置字典 """ config: Dict[str, Any] = { "topP": 1, @@ -467,73 +438,7 @@ def build_generation_config(payload: Dict[str, Any]) -> tuple[Dict[str, Any], bo if isinstance(stop_sequences, list) and stop_sequences: config["stopSequences"] = config["stopSequences"] + [str(s) for s in stop_sequences] - # Thinking 配置处理 - should_include_thinking = False - if "thinking" in payload: - thinking_value = payload.get("thinking") - if thinking_value is not None: - thinking_config = get_thinking_config(thinking_value) - include_thoughts = bool(thinking_config.get("includeThoughts", False)) - - # 检查最后一条 assistant 消息的首个块类型 - last_assistant_first_block_type = None - for msg in reversed(payload.get("messages") or []): - if not isinstance(msg, dict): - continue - if msg.get("role") != "assistant": - continue - content = msg.get("content") - if not isinstance(content, list) or not content: - continue - first_block = content[0] - if isinstance(first_block, dict): - last_assistant_first_block_type = first_block.get("type") - else: - last_assistant_first_block_type = None - break - - if include_thoughts and last_assistant_first_block_type not in { - None, "thinking", "redacted_thinking", - }: - if _anthropic_debug_enabled(): - log.info( - "[ANTHROPIC][thinking] 请求显式启用 thinking,但历史 messages 未回放 " - "满足约束的 assistant thinking/redacted_thinking 起始块,已跳过下发 thinkingConfig" - ) - return config, False - - # 处理 thinkingBudget 与 max_tokens 的关系 - if include_thoughts and isinstance(max_tokens, int): - budget = thinking_config.get("thinkingBudget") - if isinstance(budget, int) and budget >= max_tokens: - adjusted_budget = max(0, max_tokens - 1) - if adjusted_budget <= 0: - if _anthropic_debug_enabled(): - log.info( - "[ANTHROPIC][thinking] thinkingBudget>=max_tokens 且无法下调到正数," - "已跳过下发 thinkingConfig" - ) - return config, False - if _anthropic_debug_enabled(): - log.info( - f"[ANTHROPIC][thinking] thinkingBudget>=max_tokens,自动下调 budget: " - f"{budget} -> {adjusted_budget}(max_tokens={max_tokens})" - ) - thinking_config["thinkingBudget"] = adjusted_budget - - config["thinkingConfig"] = thinking_config - should_include_thinking = include_thoughts - if _anthropic_debug_enabled(): - log.info( - f"[ANTHROPIC][thinking] 已下发 thinkingConfig: includeThoughts=" - f"{thinking_config.get('includeThoughts')}, thinkingBudget=" - f"{thinking_config.get('thinkingBudget')}" - ) - else: - if _anthropic_debug_enabled(): - log.info("[ANTHROPIC][thinking] thinking=null,视为未启用 thinking") - - return config, should_include_thinking + return config # ============================================================================ @@ -565,11 +470,11 @@ async def anthropic_to_gemini_request(payload: Dict[str, Any]) -> Dict[str, Any] if not isinstance(messages, list): messages = [] - # 构建生成配置(包含thinking配置) - generation_config, should_include_thinking = build_generation_config(payload) + # 构建生成配置 + generation_config = build_generation_config(payload) - # 转换消息内容 - contents = convert_messages_to_contents(messages, include_thinking=should_include_thinking) + # 转换消息内容(始终包含thinking块,由响应端处理) + contents = convert_messages_to_contents(messages, include_thinking=True) contents = reorganize_tool_messages(contents) # 转换工具 diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 8546fe1d6..5ca67cb45 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -268,7 +268,7 @@ async def normalize_gemini_request( if is_thinking_model(model): if "thinkingConfig" not in generation_config: generation_config["thinkingConfig"] = { - "thinkingBudget": 32768, + "thinkingBudget": 1024, "includeThoughts": return_thoughts } # 移除 -thinking 后缀 @@ -289,15 +289,15 @@ async def normalize_gemini_request( # 2. 参数范围限制 if generation_config: max_tokens = generation_config.get("maxOutputTokens") - if max_tokens is not None and max_tokens > 65535: + if max_tokens is not None: generation_config["maxOutputTokens"] = 65535 top_k = generation_config.get("topK") - if top_k is not None and top_k > 64: + if top_k is not None: generation_config["topK"] = 64 # 3. 工具清理 - if tools and mode == "antigravity": + if tools: result["tools"] = clean_tools_for_gemini(tools) if generation_config: From 6b6de46c2e5a1cd3e444234be186705564f9b782 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 14:39:20 +0000 Subject: [PATCH 009/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index de54dda36..b25e3733b 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=75b84589ad4f33ced012ab4f9b5f6e899298f13d -short_hash=75b8458 +full_hash=86defb026605e3d374d2e8da1dac76c8ba7c2901 +short_hash=86defb0 message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-10 21:32:49 +0800 +date=2026-01-10 22:39:06 +0800 From 0e8e1830b424572d19dfd6079bfd1380569d1baf Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 10 Jan 2026 23:43:59 +0800 Subject: [PATCH 010/211] Update gemini_fix.py --- src/converter/openai2gemini.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 3652c2e04..777227568 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -546,13 +546,24 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di else tool_call["function"]["arguments"] ) - parts.append({ + # 解码工具ID和thoughtSignature + encoded_id = tool_call.get("id", "") + original_id, signature = decode_tool_id_and_signature(encoded_id) + + # 构建functionCall part + function_call_part = { "functionCall": { - "id": tool_call.get("id", ""), + "id": original_id, "name": tool_call["function"]["name"], "args": args } - }) + } + + # 如果有thoughtSignature,添加到part中 + if signature: + function_call_part["thoughtSignature"] = signature + + parts.append(function_call_part) except (json.JSONDecodeError, KeyError) as e: log.error(f"Failed to parse tool call: {e}") continue From ace021fe688029823ae223aa0a2acc0565845b47 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 15:44:08 +0000 Subject: [PATCH 011/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index b25e3733b..66d5f17f2 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=86defb026605e3d374d2e8da1dac76c8ba7c2901 -short_hash=86defb0 -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-10 22:39:06 +0800 +full_hash=0e8e1830b424572d19dfd6079bfd1380569d1baf +short_hash=0e8e183 +message=Update gemini_fix.py +date=2026-01-10 23:43:59 +0800 From 7532201e5d29b8643639397c38db72615752da3c Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 00:28:14 +0800 Subject: [PATCH 012/211] =?UTF-8?q?=E9=99=90=E5=88=B6=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E5=92=8C=E6=A8=A1=E5=9E=8B=E5=90=8D=E5=AD=97=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log.py | 2 +- src/converter/gemini_fix.py | 28 +++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/log.py b/log.py index 9305b7b8c..51f2cd85c 100644 --- a/log.py +++ b/log.py @@ -20,7 +20,7 @@ def _get_current_log_level(): """获取当前日志级别""" - level = os.getenv("LOG_LEVEL", "info").lower() + level = os.getenv("LOG_LEVEL", "debug").lower() return LOG_LEVELS.get(level, LOG_LEVELS["info"]) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 5ca67cb45..657adada8 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -6,6 +6,8 @@ from typing import Any, Dict, List, Optional +from log import log + # ==================== Gemini API 配置 ==================== # Gemini API 不支持的 JSON Schema 字段集合 @@ -212,9 +214,12 @@ async def normalize_gemini_request( result = request.copy() model = result.get("model", "") - generation_config = result.get("generationConfig", {}) + generation_config = result.get("generationConfig", {}).copy() # 创建副本避免修改原对象 tools = result.get("tools") system_instruction = result.get("systemInstruction") or result.get("system_instructions") + + # 记录原始请求 + log.debug(f"[GEMINI_FIX] 原始请求 - 模型: {model}, mode: {mode}, generationConfig: {generation_config}") # 获取配置值 return_thoughts = await get_return_thoughts_to_frontend() @@ -274,12 +279,29 @@ async def normalize_gemini_request( # 移除 -thinking 后缀 model = model.replace("-thinking", "") + import re + # 处理两种日期格式: + # 1. claude-{type}-{version}-{date} 格式,如 claude-sonnet-4-20250514 + m = re.match(r"^(claude-(?:opus|sonnet|haiku)-(?:4|4-5|3-5))-\d{8}$", model) + if m: + model = m.group(1) + # 2. claude-{version}-{type}-{date} 格式,如 claude-3-5-haiku-20241022 + m = re.match(r"^(claude-(?:3-5|3|4)-(?:opus|sonnet|haiku))-\d{8}$", model) + if m: + model = m.group(1) + # 4. 特殊模型映射 model_mapping = { "claude-opus-4-5": "claude-opus-4-5-thinking", - "claude-haiku-4": "gemini-2.5-flash" + "claude-haiku-4": "claude-sonnet-4-5-thinking", + "claude-haiku-3-5": "claude-sonnet-4-5-thinking", + "claude-3-5-haiku": "claude-sonnet-4-5-thinking", + "claude-opus-4": "claude-opus-4-5-thinking", + "claude-sonnet-4": "claude-sonnet-4-5-thinking", + } result["model"] = model_mapping.get(model, model) + log.debug(f"[ANTIGRAVITY] 映射模型: {model} -> {result['model']}") # ========== 公共处理 ========== # 1. 字段名转换 @@ -290,7 +312,7 @@ async def normalize_gemini_request( if generation_config: max_tokens = generation_config.get("maxOutputTokens") if max_tokens is not None: - generation_config["maxOutputTokens"] = 65535 + generation_config["maxOutputTokens"] = 64000 top_k = generation_config.get("topK") if top_k is not None: From 8ff9f365a26904b1c26d42e84270829e2a97a2d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 16:28:23 +0000 Subject: [PATCH 013/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 66d5f17f2..96690894c 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=0e8e1830b424572d19dfd6079bfd1380569d1baf -short_hash=0e8e183 -message=Update gemini_fix.py -date=2026-01-10 23:43:59 +0800 +full_hash=7532201e5d29b8643639397c38db72615752da3c +short_hash=7532201 +message=限制范围和模型名字处理 +date=2026-01-11 00:28:14 +0800 From 400e756e354cafc830f7b47052a85bfc888183a2 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 00:34:13 +0800 Subject: [PATCH 014/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 657adada8..a55ca92c6 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -279,29 +279,20 @@ async def normalize_gemini_request( # 移除 -thinking 后缀 model = model.replace("-thinking", "") - import re - # 处理两种日期格式: - # 1. claude-{type}-{version}-{date} 格式,如 claude-sonnet-4-20250514 - m = re.match(r"^(claude-(?:opus|sonnet|haiku)-(?:4|4-5|3-5))-\d{8}$", model) - if m: - model = m.group(1) - # 2. claude-{version}-{type}-{date} 格式,如 claude-3-5-haiku-20241022 - m = re.match(r"^(claude-(?:3-5|3|4)-(?:opus|sonnet|haiku))-\d{8}$", model) - if m: - model = m.group(1) + # 4. Claude 模型关键词映射 + # 使用关键词匹配而不是精确匹配,更灵活地处理各种变体 + original_model = model + if "opus" in model.lower(): + model = "claude-opus-4-5-thinking" + elif "sonnet" in model.lower() or "haiku" in model.lower(): + model = "claude-sonnet-4-5-thinking" + elif "claude" in model.lower(): + # Claude 模型兜底:如果包含 claude 但不是 opus/sonnet/haiku + model = "claude-sonnet-4-5-thinking" - # 4. 特殊模型映射 - model_mapping = { - "claude-opus-4-5": "claude-opus-4-5-thinking", - "claude-haiku-4": "claude-sonnet-4-5-thinking", - "claude-haiku-3-5": "claude-sonnet-4-5-thinking", - "claude-3-5-haiku": "claude-sonnet-4-5-thinking", - "claude-opus-4": "claude-opus-4-5-thinking", - "claude-sonnet-4": "claude-sonnet-4-5-thinking", - - } - result["model"] = model_mapping.get(model, model) - log.debug(f"[ANTIGRAVITY] 映射模型: {model} -> {result['model']}") + result["model"] = model + if original_model != model: + log.debug(f"[ANTIGRAVITY] 映射模型: {original_model} -> {model}") # ========== 公共处理 ========== # 1. 字段名转换 From e4eaa8484aaeb233b73e594b401701b9921ae851 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 16:49:45 +0000 Subject: [PATCH 015/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 96690894c..3dbd17851 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=7532201e5d29b8643639397c38db72615752da3c -short_hash=7532201 -message=限制范围和模型名字处理 -date=2026-01-11 00:28:14 +0800 +full_hash=cce5c2a0899099f617fc20b1147e6c17fa96c5c6 +short_hash=cce5c2a +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-11 00:49:30 +0800 From cef1f04a1a5a29843cfddfb45924f260787fc467 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 01:06:02 +0800 Subject: [PATCH 016/211] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dclaude=20code?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=B7=A5=E5=85=B7=E4=B8=8D=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- log.py | 2 +- src/converter/anthropic2gemini.py | 21 ++++- src/router/antigravity/anthropic.py | 138 ++++++++++------------------ 3 files changed, 71 insertions(+), 90 deletions(-) diff --git a/log.py b/log.py index 51f2cd85c..9305b7b8c 100644 --- a/log.py +++ b/log.py @@ -20,7 +20,7 @@ def _get_current_log_level(): """获取当前日志级别""" - level = os.getenv("LOG_LEVEL", "debug").lower() + level = os.getenv("LOG_LEVEL", "info").lower() return LOG_LEVELS.get(level, LOG_LEVELS["info"]) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index 94c3a0966..e0ec843ff 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -29,7 +29,7 @@ def _anthropic_debug_enabled() -> bool: """检查是否启用 Anthropic 调试模式""" - return str(os.getenv("ANTHROPIC_DEBUG", "")).strip().lower() in _DEBUG_TRUE + return str(os.getenv("ANTHROPIC_DEBUG", "true")).strip().lower() in _DEBUG_TRUE def _is_non_whitespace_text(value: Any) -> bool: @@ -822,7 +822,14 @@ def _close_block() -> Optional[bytes]: tool_name = fc.get("name") or "" tool_args = _remove_nulls_for_tool_input(fc.get("args", {}) or {}) + if _anthropic_debug_enabled(): + log.info( + f"[ANTHROPIC][tool_use] 处理工具调用: name={tool_name}, " + f"id={tool_id}, has_signature={signature is not None}" + ) + current_block_index += 1 + # 注意:工具调用不设置 current_block_type,因为它是独立完整的块 yield _sse_event( "content_block_start", @@ -852,6 +859,11 @@ def _close_block() -> Optional[bytes]: "content_block_stop", {"type": "content_block_stop", "index": current_block_index}, ) + # 工具调用块已完全关闭,current_block_type 保持为 None + + if _anthropic_debug_enabled(): + log.info(f"[ANTHROPIC][tool_use] 工具调用块已关闭: index={current_block_index}") + continue # 检查是否结束 @@ -869,6 +881,13 @@ def _close_block() -> Optional[bytes]: if finish_reason == "MAX_TOKENS" and not has_tool_use: stop_reason = "max_tokens" + if _anthropic_debug_enabled(): + log.info( + f"[ANTHROPIC][stream_end] 流式结束: stop_reason={stop_reason}, " + f"has_tool_use={has_tool_use}, finish_reason={finish_reason}, " + f"input_tokens={input_tokens}, output_tokens={output_tokens}" + ) + # 发送 message_delta 和 message_stop yield _sse_event( "message_delta", diff --git a/src/router/antigravity/anthropic.py b/src/router/antigravity/anthropic.py index 4ae6195f8..ba271aaf7 100644 --- a/src/router/antigravity/anthropic.py +++ b/src/router/antigravity/anthropic.py @@ -272,6 +272,7 @@ async def get_response(): async def anti_truncation_generator(): from src.converter.anti_truncation import apply_anti_truncation_to_stream from src.api.antigravity import non_stream_request + from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream max_attempts = await get_anti_truncation_max_attempts() @@ -283,105 +284,66 @@ async def anti_truncation_generator(): max_attempts ) - # yield StreamingResponse 的内容,并转换为 Anthropic 格式 - async for chunk in streaming_response.body_iterator: - if not chunk: - continue - - # 解析 Gemini SSE 格式 - chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk - - # 跳过空行和 [DONE] 标记 - if not chunk_str.strip() or chunk_str.strip() == "data: [DONE]": - continue - - # 解析 "data: {...}" 格式 - if chunk_str.startswith("data: "): - try: - # 转换为 Anthropic 格式 - from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream - # 注意:这里需要将chunk转换为async iterator - async def single_chunk_iter(): - yield chunk_str.encode('utf-8') - - async for anthropic_chunk in gemini_stream_to_anthropic_stream( - single_chunk_iter(), - real_model, - 200 - ): - if anthropic_chunk: - yield anthropic_chunk - - except Exception as e: - log.error(f"Failed to convert chunk: {e}") - continue - - # 发送结束标记 - yield "data: [DONE]\n\n".encode() + # 包装以确保是bytes流 + async def bytes_wrapper(): + async for chunk in streaming_response.body_iterator: + if isinstance(chunk, str): + yield chunk.encode('utf-8') + else: + yield chunk + + # 直接将整个流传递给转换器 + async for anthropic_chunk in gemini_stream_to_anthropic_stream( + bytes_wrapper(), + real_model, + 200 + ): + if anthropic_chunk: + yield anthropic_chunk # ========== 普通流式生成器 ========== async def normal_stream_generator(): from src.api.antigravity import stream_request from fastapi import Response + from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream # 调用 API 层的流式请求(不使用 native 模式) stream_gen = stream_request(body=api_request, native=False) - # yield所有数据,处理可能的错误Response - async for chunk in stream_gen: - # 检查是否是Response对象(错误情况) - if isinstance(chunk, Response): - # 将Response转换为SSE格式的错误消息 - error_content = chunk.body if isinstance(chunk.body, bytes) else chunk.body.encode('utf-8') - try: - gemini_error = json.loads(error_content.decode('utf-8')) - # 转换为 Anthropic 格式错误 - from src.converter.anthropic2gemini import gemini_to_anthropic_response - anthropic_error = gemini_to_anthropic_response( - gemini_error, - real_model, - chunk.status_code - ) - yield f"data: {json.dumps(anthropic_error)}\n\n".encode('utf-8') - except Exception: - yield f"data: {json.dumps({'type': 'error', 'error': {'type': 'api_error', 'message': 'Stream error'}})}\n\n".encode('utf-8') - return - else: - # 正常的bytes数据,转换为 Anthropic 格式 - chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk - - # 跳过空行 - if not chunk_str.strip(): - continue - - # 处理 [DONE] 标记 - if chunk_str.strip() == "data: [DONE]": - yield "data: [DONE]\n\n".encode('utf-8') - return - - # 解析并转换 Gemini chunk 为 Anthropic 格式 - if chunk_str.startswith("data: "): + # 包装流式生成器以处理错误响应 + async def gemini_chunk_wrapper(): + async for chunk in stream_gen: + # 检查是否是Response对象(错误情况) + if isinstance(chunk, Response): + # 错误响应,不进行转换,直接传递 + error_content = chunk.body if isinstance(chunk.body, bytes) else chunk.body.encode('utf-8') try: - # 转换为 Anthropic 格式 - from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream - # 创建单个chunk的异步迭代器 - async def single_chunk_iter(): - yield chunk_str.encode('utf-8') - - async for anthropic_chunk in gemini_stream_to_anthropic_stream( - single_chunk_iter(), + gemini_error = json.loads(error_content.decode('utf-8')) + from src.converter.anthropic2gemini import gemini_to_anthropic_response + anthropic_error = gemini_to_anthropic_response( + gemini_error, real_model, - 200 - ): - if anthropic_chunk: - yield anthropic_chunk - - except Exception as e: - log.error(f"Failed to convert chunk: {e}") - continue - - # 发送结束标记 - yield "data: [DONE]\n\n".encode('utf-8') + chunk.status_code + ) + yield f"data: {json.dumps(anthropic_error)}\n\n".encode('utf-8') + except Exception: + yield f"data: {json.dumps({'type': 'error', 'error': {'type': 'api_error', 'message': 'Stream error'}})}\n\n".encode('utf-8') + return + else: + # 确保是bytes类型 + if isinstance(chunk, str): + yield chunk.encode('utf-8') + else: + yield chunk + + # 使用转换器处理整个流 + async for anthropic_chunk in gemini_stream_to_anthropic_stream( + gemini_chunk_wrapper(), + real_model, + 200 + ): + if anthropic_chunk: + yield anthropic_chunk # ========== 根据模式选择生成器 ========== if use_fake_streaming: From 3526f71fa178540818b826f99604356fd586e0b5 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 01:11:31 +0800 Subject: [PATCH 017/211] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 25 +++++- src/api/geminicli.py | 25 +++++- src/router/geminicli/anthropic.py | 142 +++++++++++------------------- 3 files changed, 92 insertions(+), 100 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 0afd4c0d7..a50d70a71 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -166,7 +166,12 @@ async def stream_request( # 如果错误码是429或者不在禁用码当中,做好记录后进行重试 if status_code == 429 or status_code not in DISABLE_ERROR_CODES: - log.warning(f"[ANTIGRAVITY STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}") + # 解析错误响应内容 + try: + error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) + log.warning(f"[ANTIGRAVITY STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") + except Exception: + log.warning(f"[ANTIGRAVITY STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}") # 记录错误 cooldown_until = None @@ -231,7 +236,11 @@ async def stream_request( return else: # 错误码在禁用码当中,直接返回,无需重试 - log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") + try: + error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) + log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") + except Exception: + log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="antigravity", model_key=model_name @@ -381,7 +390,11 @@ async def non_stream_request( # 判断是否需要重试 if status_code == 429 or status_code not in DISABLE_ERROR_CODES: - log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}") + try: + error_text = response.text + log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") + except Exception: + log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}") # 记录错误 cooldown_until = None @@ -443,7 +456,11 @@ async def non_stream_request( return last_error_response else: # 错误码在禁用码当中,直接返回,无需重试 - log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") + try: + error_text = response.text + log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") + except Exception: + log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="antigravity", model_key=model_name diff --git a/src/api/geminicli.py b/src/api/geminicli.py index bba969926..21dcce3af 100644 --- a/src/api/geminicli.py +++ b/src/api/geminicli.py @@ -183,7 +183,12 @@ async def stream_request( # 如果错误码是429或者不在禁用码当中,做好记录后进行重试 if status_code == 429 or status_code not in DISABLE_ERROR_CODES: - log.warning(f"流式请求失败 (status={status_code}), 凭证: {current_file}") + # 解析错误响应内容 + try: + error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) + log.warning(f"流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") + except Exception: + log.warning(f"流式请求失败 (status={status_code}), 凭证: {current_file}") # 记录错误 cooldown_until = None @@ -240,7 +245,11 @@ async def stream_request( return else: # 错误码在禁用码当中,直接返回,无需重试 - log.error(f"流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") + try: + error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) + log.error(f"流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") + except Exception: + log.error(f"流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="geminicli", model_key=model_group @@ -377,7 +386,11 @@ async def non_stream_request( # 判断是否需要重试 if status_code == 429 or status_code not in DISABLE_ERROR_CODES: - log.warning(f"非流式请求失败 (status={status_code}), 凭证: {current_file}") + try: + error_text = response.text + log.warning(f"非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") + except Exception: + log.warning(f"非流式请求失败 (status={status_code}), 凭证: {current_file}") # 记录错误 cooldown_until = None @@ -432,7 +445,11 @@ async def non_stream_request( return last_error_response else: # 错误码在禁用码当中,直接返回,无需重试 - log.error(f"非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") + try: + error_text = response.text + log.error(f"非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") + except Exception: + log.error(f"非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="geminicli", model_key=model_group diff --git a/src/router/geminicli/anthropic.py b/src/router/geminicli/anthropic.py index 5aee31434..1a9a2b5c6 100644 --- a/src/router/geminicli/anthropic.py +++ b/src/router/geminicli/anthropic.py @@ -272,6 +272,7 @@ async def get_response(): async def anti_truncation_generator(): from src.converter.anti_truncation import apply_anti_truncation_to_stream from src.api.geminicli import non_stream_request + from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream max_attempts = await get_anti_truncation_max_attempts() @@ -283,109 +284,66 @@ async def anti_truncation_generator(): max_attempts ) - # 转换为 Anthropic 格式 - import uuid - response_id = str(uuid.uuid4()) - - # yield StreamingResponse 的内容,并转换为 Anthropic 格式 - async for chunk in streaming_response.body_iterator: - if not chunk: - continue - - # 解析 Gemini SSE 格式 - chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk - - # 跳过空行和 [DONE] 标记 - if not chunk_str.strip() or chunk_str.strip() == "data: [DONE]": - continue - - # 解析 "data: {...}" 格式 - if chunk_str.startswith("data: "): - try: - # 转换为 Anthropic 格式 - from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream - # 注意:这里需要将chunk转换为async iterator - async def single_chunk_iter(): - yield chunk_str.encode('utf-8') - - async for anthropic_chunk in gemini_stream_to_anthropic_stream( - single_chunk_iter(), - real_model, - 200 - ): - if anthropic_chunk: - yield anthropic_chunk - - except Exception as e: - log.error(f"Failed to convert chunk: {e}") - continue - - # 发送结束标记 - yield "data: [DONE]\n\n".encode() + # 包装以确保是bytes流 + async def bytes_wrapper(): + async for chunk in streaming_response.body_iterator: + if isinstance(chunk, str): + yield chunk.encode('utf-8') + else: + yield chunk + + # 直接将整个流传递给转换器 + async for anthropic_chunk in gemini_stream_to_anthropic_stream( + bytes_wrapper(), + real_model, + 200 + ): + if anthropic_chunk: + yield anthropic_chunk # ========== 普通流式生成器 ========== async def normal_stream_generator(): from src.api.geminicli import stream_request from fastapi import Response + from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream # 调用 API 层的流式请求(不使用 native 模式) stream_gen = stream_request(body=api_request, native=False) - # yield所有数据,处理可能的错误Response - async for chunk in stream_gen: - # 检查是否是Response对象(错误情况) - if isinstance(chunk, Response): - # 将Response转换为SSE格式的错误消息 - error_content = chunk.body if isinstance(chunk.body, bytes) else chunk.body.encode('utf-8') - try: - gemini_error = json.loads(error_content.decode('utf-8')) - # 转换为 Anthropic 格式错误 - from src.converter.anthropic2gemini import gemini_to_anthropic_response - anthropic_error = gemini_to_anthropic_response( - gemini_error, - real_model, - chunk.status_code - ) - yield f"data: {json.dumps(anthropic_error)}\n\n".encode('utf-8') - except Exception: - yield f"data: {json.dumps({'type': 'error', 'error': {'type': 'api_error', 'message': 'Stream error'}})}\n\n".encode('utf-8') - return - else: - # 正常的bytes数据,转换为 Anthropic 格式 - chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk - - # 跳过空行 - if not chunk_str.strip(): - continue - - # 处理 [DONE] 标记 - if chunk_str.strip() == "data: [DONE]": - yield "data: [DONE]\n\n".encode('utf-8') - return - - # 解析并转换 Gemini chunk 为 Anthropic 格式 - if chunk_str.startswith("data: "): + # 包装流式生成器以处理错误响应 + async def gemini_chunk_wrapper(): + async for chunk in stream_gen: + # 检查是否是Response对象(错误情况) + if isinstance(chunk, Response): + # 错误响应,不进行转换,直接传递 + error_content = chunk.body if isinstance(chunk.body, bytes) else chunk.body.encode('utf-8') try: - # 转换为 Anthropic 格式 - from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream - # 创建单个chunk的异步迭代器 - async def single_chunk_iter(): - yield chunk_str.encode('utf-8') - - async for anthropic_chunk in gemini_stream_to_anthropic_stream( - single_chunk_iter(), + gemini_error = json.loads(error_content.decode('utf-8')) + from src.converter.anthropic2gemini import gemini_to_anthropic_response + anthropic_error = gemini_to_anthropic_response( + gemini_error, real_model, - 200 - ): - if anthropic_chunk: - yield anthropic_chunk - - except Exception as e: - log.error(f"Failed to convert chunk: {e}") - continue - - # 发送结束标记 - yield "data: [DONE]\n\n".encode('utf-8') + chunk.status_code + ) + yield f"data: {json.dumps(anthropic_error)}\n\n".encode('utf-8') + except Exception: + yield f"data: {json.dumps({'type': 'error', 'error': {'type': 'api_error', 'message': 'Stream error'}})}\n\n".encode('utf-8') + return + else: + # 确保是bytes类型 + if isinstance(chunk, str): + yield chunk.encode('utf-8') + else: + yield chunk + + # 使用转换器处理整个流 + async for anthropic_chunk in gemini_stream_to_anthropic_stream( + gemini_chunk_wrapper(), + real_model, + 200 + ): + if anthropic_chunk: + yield anthropic_chunk # ========== 根据模式选择生成器 ========== if use_fake_streaming: From d499b8f66da327c045da92fbe432d641cf502a0e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 17:11:44 +0000 Subject: [PATCH 018/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 3dbd17851..9ea08c9d4 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=cce5c2a0899099f617fc20b1147e6c17fa96c5c6 -short_hash=cce5c2a -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 00:49:30 +0800 +full_hash=3526f71fa178540818b826f99604356fd586e0b5 +short_hash=3526f71 +message=增加更多日志 +date=2026-01-11 01:11:31 +0800 From 382400792f70e9c18119ba9db6a106c53bc836e4 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 01:15:00 +0800 Subject: [PATCH 019/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index a55ca92c6..107de982f 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -143,16 +143,18 @@ def prepare_image_generation_request( def get_base_model_name(model_name: str) -> str: """移除模型名称中的后缀,返回基础模型名""" - suffixes = ["-maxthinking", "-nothinking", "-think", "-search"] + # 按照从长到短的顺序排列,避免 -think 先于 -maxthinking 被匹配 + suffixes = ["-maxthinking", "-nothinking", "-search", "-think"] result = model_name changed = True + # 持续循环直到没有任何后缀可以移除 while changed: changed = False for suffix in suffixes: if result.endswith(suffix): result = result[:-len(suffix)] changed = True - break + # 不使用 break,继续检查是否还有其他后缀 return result From b4ceb9253e86699673adaceeed2615f198ac9845 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 17:15:43 +0000 Subject: [PATCH 020/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 9ea08c9d4..8c9173f8e 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=3526f71fa178540818b826f99604356fd586e0b5 -short_hash=3526f71 -message=增加更多日志 -date=2026-01-11 01:11:31 +0800 +full_hash=f181594bedb94f4e18ef7395741fb7559e2ff9d8 +short_hash=f181594 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-11 01:15:11 +0800 From f966035590d68a70a4c31871fbf9a8008c0b39e8 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 01:29:44 +0800 Subject: [PATCH 021/211] =?UTF-8?q?=E6=B8=85=E7=90=86=E7=A9=BAparts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 275 +++++++++++++++++++++--------------- src/converter/gemini_fix.py | 32 +++++ 2 files changed, 192 insertions(+), 115 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index a50d70a71..096094244 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -148,9 +148,27 @@ async def stream_request( DISABLE_ERROR_CODES = await get_auto_ban_error_codes() # 禁用凭证的错误码 last_error_response = None # 记录最后一次的错误响应 + + # 内部函数:获取新凭证并更新headers + async def refresh_credential(): + nonlocal current_file, access_token, auth_headers + cred_result = await credential_manager.get_valid_credential( + mode="antigravity", model_key=model_name + ) + if not cred_result: + return None + current_file, credential_data = cred_result + access_token = credential_data.get("access_token") or credential_data.get("token") + if not access_token: + return None + auth_headers = build_antigravity_headers(access_token, model_name) + if headers: + auth_headers.update(headers) + return True for attempt in range(max_retries + 1): success_recorded = False # 标记是否已记录成功 + need_retry = False # 标记是否需要重试 try: async for chunk in stream_post_async( @@ -196,39 +214,8 @@ async def stream_request( ) if should_retry and attempt < max_retries: - # 重新获取凭证并重试 - log.info(f"[ANTIGRAVITY STREAM] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") - await asyncio.sleep(retry_interval) - - # 获取新凭证 - cred_result = await credential_manager.get_valid_credential( - mode="antigravity", model_key=model_name - ) - if not cred_result: - log.error("[ANTIGRAVITY STREAM] 重试时无可用凭证") - yield Response( - content=json.dumps({"error": "当前无可用凭证"}), - status_code=500, - media_type="application/json" - ) - return - - current_file, credential_data = cred_result - access_token = credential_data.get("access_token") or credential_data.get("token") - - if not access_token: - log.error(f"[ANTIGRAVITY STREAM] No access token in credential: {current_file}") - yield Response( - content=json.dumps({"error": "凭证中没有访问令牌"}), - status_code=500, - media_type="application/json" - ) - return - - auth_headers = build_antigravity_headers(access_token, model_name) - if headers: - auth_headers.update(headers) - break # 跳出内层循环,重新请求 + need_retry = True + break # 跳出内层循环,准备重试 else: # 不重试,直接返回原始错误 log.error(f"[ANTIGRAVITY STREAM] 达到最大重试次数或不应重试,返回原始错误") @@ -265,10 +252,43 @@ async def stream_request( yield chunk - # 流式请求成功完成,退出重试循环 + # 流式请求完成,检查结果 if success_recorded: log.info(f"[ANTIGRAVITY STREAM] 流式响应完成,模型: {model_name}") - return + return + elif not need_retry: + # 没有收到任何数据(空回复),需要重试 + log.warning(f"[ANTIGRAVITY STREAM] 收到空回复,无任何内容,凭证: {current_file}") + await record_api_call_error( + credential_manager, current_file, 200, + None, mode="antigravity", model_key=model_name + ) + + if attempt < max_retries: + need_retry = True + else: + log.error(f"[ANTIGRAVITY STREAM] 空回复达到最大重试次数") + yield Response( + content=json.dumps({"error": "服务返回空回复"}), + status_code=500, + media_type="application/json" + ) + return + + # 统一处理重试 + if need_retry: + log.info(f"[ANTIGRAVITY STREAM] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") + await asyncio.sleep(retry_interval) + + if not await refresh_credential(): + log.error("[ANTIGRAVITY STREAM] 重试时无可用凭证或令牌") + yield Response( + content=json.dumps({"error": "当前无可用凭证"}), + status_code=500, + media_type="application/json" + ) + return + continue # 重试 except Exception as e: log.error(f"[ANTIGRAVITY STREAM] 流式请求异常: {e}, 凭证: {current_file}") @@ -358,8 +378,27 @@ async def non_stream_request( DISABLE_ERROR_CODES = await get_auto_ban_error_codes() # 禁用凭证的错误码 last_error_response = None # 记录最后一次的错误响应 + + # 内部函数:获取新凭证并更新headers + async def refresh_credential(): + nonlocal current_file, access_token, auth_headers + cred_result = await credential_manager.get_valid_credential( + mode="antigravity", model_key=model_name + ) + if not cred_result: + return None + current_file, credential_data = cred_result + access_token = credential_data.get("access_token") or credential_data.get("token") + if not access_token: + return None + auth_headers = build_antigravity_headers(access_token, model_name) + if headers: + auth_headers.update(headers) + return True for attempt in range(max_retries + 1): + need_retry = False # 标记是否需要重试 + try: response = await post_async( url=target_url, @@ -372,100 +411,106 @@ async def non_stream_request( # 成功 if status_code == 200: - await record_api_call_success( - credential_manager, current_file, mode="antigravity", model_key=model_name - ) - return Response( + # 检查是否为空回复 + if not response.content or len(response.content) == 0: + log.warning(f"[ANTIGRAVITY] 收到200响应但内容为空,凭证: {current_file}") + + # 记录错误 + await record_api_call_error( + credential_manager, current_file, 200, + None, mode="antigravity", model_key=model_name + ) + + if attempt < max_retries: + need_retry = True + else: + log.error(f"[ANTIGRAVITY] 空回复达到最大重试次数") + return Response( + content=json.dumps({"error": "服务返回空回复"}), + status_code=500, + media_type="application/json" + ) + else: + # 正常响应 + await record_api_call_success( + credential_manager, current_file, mode="antigravity", model_key=model_name + ) + return Response( + content=response.content, + status_code=200, + headers=dict(response.headers) + ) + + # 失败 - 记录最后一次错误 + if status_code != 200: + last_error_response = Response( content=response.content, - status_code=200, + status_code=status_code, headers=dict(response.headers) ) - # 失败 - 记录最后一次错误 - last_error_response = Response( - content=response.content, - status_code=status_code, - headers=dict(response.headers) - ) - - # 判断是否需要重试 - if status_code == 429 or status_code not in DISABLE_ERROR_CODES: - try: - error_text = response.text - log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") - except Exception: - log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}") - - # 记录错误 - cooldown_until = None - if status_code == 429: - # 尝试解析冷却时间 + # 判断是否需要重试 + if status_code == 429 or status_code not in DISABLE_ERROR_CODES: try: error_text = response.text - cooldown_until = await parse_and_log_cooldown(error_text, mode="antigravity") + log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") except Exception: - pass - - await record_api_call_error( - credential_manager, current_file, status_code, - cooldown_until, mode="antigravity", model_key=model_name - ) - - # 检查是否应该重试 - should_retry = await handle_error_with_retry( - credential_manager, status_code, current_file, - retry_config["retry_enabled"], attempt, max_retries, retry_interval, - mode="antigravity" - ) + log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}") - if should_retry and attempt < max_retries: - # 重新获取凭证并重试 - log.info(f"[ANTIGRAVITY] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") - await asyncio.sleep(retry_interval) + # 记录错误 + cooldown_until = None + if status_code == 429: + # 尝试解析冷却时间 + try: + error_text = response.text + cooldown_until = await parse_and_log_cooldown(error_text, mode="antigravity") + except Exception: + pass - # 获取新凭证 - cred_result = await credential_manager.get_valid_credential( - mode="antigravity", model_key=model_name + await record_api_call_error( + credential_manager, current_file, status_code, + cooldown_until, mode="antigravity", model_key=model_name ) - if not cred_result: - log.error("[ANTIGRAVITY] 重试时无可用凭证") - return Response( - content=json.dumps({"error": "当前无可用凭证"}), - status_code=500, - media_type="application/json" - ) - - current_file, credential_data = cred_result - access_token = credential_data.get("access_token") or credential_data.get("token") - if not access_token: - log.error(f"[ANTIGRAVITY] No access token in credential: {current_file}") - return Response( - content=json.dumps({"error": "凭证中没有访问令牌"}), - status_code=500, - media_type="application/json" - ) + # 检查是否应该重试 + should_retry = await handle_error_with_retry( + credential_manager, status_code, current_file, + retry_config["retry_enabled"], attempt, max_retries, retry_interval, + mode="antigravity" + ) - auth_headers = build_antigravity_headers(access_token, model_name) - if headers: - auth_headers.update(headers) - continue # 重试 + if should_retry and attempt < max_retries: + need_retry = True + else: + # 不重试,直接返回原始错误 + log.error(f"[ANTIGRAVITY] 达到最大重试次数或不应重试,返回原始错误") + return last_error_response else: - # 不重试,直接返回原始错误 - log.error(f"[ANTIGRAVITY] 达到最大重试次数或不应重试,返回原始错误") + # 错误码在禁用码当中,直接返回,无需重试 + try: + error_text = response.text + log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") + except Exception: + log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") + await record_api_call_error( + credential_manager, current_file, status_code, + None, mode="antigravity", model_key=model_name + ) return last_error_response - else: - # 错误码在禁用码当中,直接返回,无需重试 - try: - error_text = response.text - log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") - except Exception: - log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") - await record_api_call_error( - credential_manager, current_file, status_code, - None, mode="antigravity", model_key=model_name - ) - return last_error_response + + # 统一处理重试 + if need_retry: + log.info(f"[ANTIGRAVITY] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") + await asyncio.sleep(retry_interval) + + if not await refresh_credential(): + log.error("[ANTIGRAVITY] 重试时无可用凭证或令牌") + return Response( + content=json.dumps({"error": "当前无可用凭证"}), + status_code=500, + media_type="application/json" + ) + continue # 重试 except Exception as e: log.error(f"[ANTIGRAVITY] 非流式请求异常: {e}, 凭证: {current_file}") diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 107de982f..0f5ef4b09 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -315,6 +315,38 @@ async def normalize_gemini_request( if tools: result["tools"] = clean_tools_for_gemini(tools) + # 4. 清理空的 parts(修复 400 错误:required oneof field 'data' must have one initialized field) + if "contents" in result: + cleaned_contents = [] + for content in result["contents"]: + if isinstance(content, dict) and "parts" in content: + # 过滤掉空的或无效的 parts + valid_parts = [] + for part in content["parts"]: + if not isinstance(part, dict): + continue + # 检查 part 是否有有效的数据字段 + has_valid_data = any( + key in part and part[key] + for key in ["text", "inlineData", "fileData", "functionCall", "functionResponse"] + ) + if has_valid_data: + valid_parts.append(part) + else: + log.warning(f"[GEMINI_FIX] 移除空的 part: {part}") + + # 只添加有有效 parts 的 content + if valid_parts: + cleaned_content = content.copy() + cleaned_content["parts"] = valid_parts + cleaned_contents.append(cleaned_content) + else: + log.warning(f"[GEMINI_FIX] 跳过没有有效 parts 的 content: {content.get('role')}") + else: + cleaned_contents.append(content) + + result["contents"] = cleaned_contents + if generation_config: result["generationConfig"] = generation_config From eac7efc5e043d18cd427056188be18b611d75efc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 17:31:14 +0000 Subject: [PATCH 022/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 8c9173f8e..387671b13 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=f181594bedb94f4e18ef7395741fb7559e2ff9d8 -short_hash=f181594 +full_hash=be9c97d4575f04ef2a8a6201a706272761c1cf6a +short_hash=be9c97d message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 01:15:11 +0800 +date=2026-01-11 01:31:02 +0800 From f541bebe1118309914ac123b54ddd6d13b8b0dc9 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 01:35:35 +0800 Subject: [PATCH 023/211] Update openai2gemini.py --- src/converter/openai2gemini.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 777227568..c7fcad49a 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -262,7 +262,7 @@ def convert_openai_tools_to_gemini(openai_tools: List) -> List[Dict[str, Any]]: # 如果名称被修改了,记录日志 if normalized_name != original_name: - log.info(f"Function name normalized: '{original_name}' -> '{normalized_name}'") + log.debug(f"Function name normalized: '{original_name}' -> '{normalized_name}'") # 构建 Gemini function declaration declaration = { From a1388e17bb9c1b4c6eaf831b1f704a2ec5a1045d Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 01:50:45 +0800 Subject: [PATCH 024/211] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E6=8A=97=E6=88=AA=E6=96=AD=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/antigravity/anthropic.py | 25 +++++++++++++------ src/router/antigravity/gemini.py | 27 +++++++++++++------- src/router/antigravity/openai.py | 38 ++++++++++++++++++++--------- src/router/geminicli/anthropic.py | 25 +++++++++++++------ src/router/geminicli/gemini.py | 27 +++++++++++++------- src/router/geminicli/openai.py | 38 ++++++++++++++++++++--------- 6 files changed, 122 insertions(+), 58 deletions(-) diff --git a/src/router/antigravity/anthropic.py b/src/router/antigravity/anthropic.py index ba271aaf7..308986455 100644 --- a/src/router/antigravity/anthropic.py +++ b/src/router/antigravity/anthropic.py @@ -270,23 +270,32 @@ async def get_response(): # ========== 流式抗截断生成器 ========== async def anti_truncation_generator(): - from src.converter.anti_truncation import apply_anti_truncation_to_stream - from src.api.antigravity import non_stream_request + from src.converter.anti_truncation import AntiTruncationStreamProcessor + from src.api.antigravity import stream_request + from src.converter.anti_truncation import apply_anti_truncation from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream max_attempts = await get_anti_truncation_max_attempts() - # 使用 apply_anti_truncation_to_stream 包装请求 - # 这个函数会自动处理所有的续传逻辑 - streaming_response = await apply_anti_truncation_to_stream( - non_stream_request, - api_request, + # 首先对payload应用反截断指令 + anti_truncation_payload = apply_anti_truncation(api_request) + + # 定义流式请求函数(返回 StreamingResponse) + async def stream_request_wrapper(payload): + # stream_request 返回异步生成器,需要包装成 StreamingResponse + stream_gen = stream_request(body=payload, native=False) + return StreamingResponse(stream_gen, media_type="text/event-stream") + + # 创建反截断处理器 + processor = AntiTruncationStreamProcessor( + stream_request_wrapper, + anti_truncation_payload, max_attempts ) # 包装以确保是bytes流 async def bytes_wrapper(): - async for chunk in streaming_response.body_iterator: + async for chunk in processor.process_stream(): if isinstance(chunk, str): yield chunk.encode('utf-8') else: diff --git a/src/router/antigravity/gemini.py b/src/router/antigravity/gemini.py index 9befccdaa..a09417fe6 100644 --- a/src/router/antigravity/gemini.py +++ b/src/router/antigravity/gemini.py @@ -253,8 +253,9 @@ async def get_response(): # ========== 流式抗截断生成器 ========== async def anti_truncation_generator(): from src.converter.gemini_fix import normalize_gemini_request - from src.converter.anti_truncation import apply_anti_truncation_to_stream - from src.api.antigravity import non_stream_request + from src.converter.anti_truncation import AntiTruncationStreamProcessor + from src.converter.anti_truncation import apply_anti_truncation + from src.api.antigravity import stream_request # 先进行基础标准化 normalized_req = await normalize_gemini_request(normalized_dict.copy(), mode="antigravity") @@ -267,16 +268,24 @@ async def anti_truncation_generator(): max_attempts = await get_anti_truncation_max_attempts() - # 使用 apply_anti_truncation_to_stream 包装请求 - # 这个函数会自动处理所有的续传逻辑 - streaming_response = await apply_anti_truncation_to_stream( - non_stream_request, - api_request, + # 首先对payload应用反截断指令 + anti_truncation_payload = apply_anti_truncation(api_request) + + # 定义流式请求函数(返回 StreamingResponse) + async def stream_request_wrapper(payload): + # stream_request 返回异步生成器,需要包装成 StreamingResponse + stream_gen = stream_request(body=payload, native=False) + return StreamingResponse(stream_gen, media_type="text/event-stream") + + # 创建反截断处理器 + processor = AntiTruncationStreamProcessor( + stream_request_wrapper, + anti_truncation_payload, max_attempts ) - # yield StreamingResponse 的内容 - async for chunk in streaming_response.body_iterator: + # 直接迭代 process_stream() 生成器 + async for chunk in processor.process_stream(): yield chunk # ========== 普通流式生成器 ========== diff --git a/src/router/antigravity/openai.py b/src/router/antigravity/openai.py index 5b9628236..ac8b017da 100644 --- a/src/router/antigravity/openai.py +++ b/src/router/antigravity/openai.py @@ -274,16 +274,25 @@ async def get_response(): # ========== 流式抗截断生成器 ========== async def anti_truncation_generator(): - from src.converter.anti_truncation import apply_anti_truncation_to_stream - from src.api.antigravity import non_stream_request + from src.converter.anti_truncation import AntiTruncationStreamProcessor + from src.api.antigravity import stream_request + from src.converter.anti_truncation import apply_anti_truncation max_attempts = await get_anti_truncation_max_attempts() - # 使用 apply_anti_truncation_to_stream 包装请求 - # 这个函数会自动处理所有的续传逻辑 - streaming_response = await apply_anti_truncation_to_stream( - non_stream_request, - api_request, + # 首先对payload应用反截断指令 + anti_truncation_payload = apply_anti_truncation(api_request) + + # 定义流式请求函数(返回 StreamingResponse) + async def stream_request_wrapper(payload): + # stream_request 返回异步生成器,需要包装成 StreamingResponse + stream_gen = stream_request(body=payload, native=False) + return StreamingResponse(stream_gen, media_type="text/event-stream") + + # 创建反截断处理器 + processor = AntiTruncationStreamProcessor( + stream_request_wrapper, + anti_truncation_payload, max_attempts ) @@ -291,18 +300,23 @@ async def anti_truncation_generator(): import uuid response_id = str(uuid.uuid4()) - # yield StreamingResponse 的内容,并转换为 OpenAI 格式 - async for chunk in streaming_response.body_iterator: + # 直接迭代 process_stream() 生成器,并转换为 OpenAI 格式 + async for chunk in processor.process_stream(): if not chunk: continue # 解析 Gemini SSE 格式 chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk - # 跳过空行和 [DONE] 标记 - if not chunk_str.strip() or chunk_str.strip() == "data: [DONE]": + # 跳过空行 + if not chunk_str.strip(): continue + # 处理 [DONE] 标记 + if chunk_str.strip() == "data: [DONE]": + yield "data: [DONE]\n\n".encode('utf-8') + return + # 解析 "data: {...}" 格式 if chunk_str.startswith("data: "): try: @@ -322,7 +336,7 @@ async def anti_truncation_generator(): continue # 发送结束标记 - yield "data: [DONE]\n\n".encode() + yield "data: [DONE]\n\n".encode('utf-8') # ========== 普通流式生成器 ========== async def normal_stream_generator(): diff --git a/src/router/geminicli/anthropic.py b/src/router/geminicli/anthropic.py index 1a9a2b5c6..c99958b57 100644 --- a/src/router/geminicli/anthropic.py +++ b/src/router/geminicli/anthropic.py @@ -270,23 +270,32 @@ async def get_response(): # ========== 流式抗截断生成器 ========== async def anti_truncation_generator(): - from src.converter.anti_truncation import apply_anti_truncation_to_stream - from src.api.geminicli import non_stream_request + from src.converter.anti_truncation import AntiTruncationStreamProcessor + from src.api.geminicli import stream_request + from src.converter.anti_truncation import apply_anti_truncation from src.converter.anthropic2gemini import gemini_stream_to_anthropic_stream max_attempts = await get_anti_truncation_max_attempts() - # 使用 apply_anti_truncation_to_stream 包装请求 - # 这个函数会自动处理所有的续传逻辑 - streaming_response = await apply_anti_truncation_to_stream( - non_stream_request, - api_request, + # 首先对payload应用反截断指令 + anti_truncation_payload = apply_anti_truncation(api_request) + + # 定义流式请求函数(返回 StreamingResponse) + async def stream_request_wrapper(payload): + # stream_request 返回异步生成器,需要包装成 StreamingResponse + stream_gen = stream_request(body=payload, native=False) + return StreamingResponse(stream_gen, media_type="text/event-stream") + + # 创建反截断处理器 + processor = AntiTruncationStreamProcessor( + stream_request_wrapper, + anti_truncation_payload, max_attempts ) # 包装以确保是bytes流 async def bytes_wrapper(): - async for chunk in streaming_response.body_iterator: + async for chunk in processor.process_stream(): if isinstance(chunk, str): yield chunk.encode('utf-8') else: diff --git a/src/router/geminicli/gemini.py b/src/router/geminicli/gemini.py index b0f4a129c..b40220e65 100644 --- a/src/router/geminicli/gemini.py +++ b/src/router/geminicli/gemini.py @@ -252,8 +252,9 @@ async def get_response(): # ========== 流式抗截断生成器 ========== async def anti_truncation_generator(): from src.converter.gemini_fix import normalize_gemini_request - from src.converter.anti_truncation import apply_anti_truncation_to_stream - from src.api.geminicli import non_stream_request + from src.converter.anti_truncation import AntiTruncationStreamProcessor + from src.converter.anti_truncation import apply_anti_truncation + from src.api.geminicli import stream_request # 先进行基础标准化 normalized_req = await normalize_gemini_request(normalized_dict.copy(), mode="geminicli") @@ -266,16 +267,24 @@ async def anti_truncation_generator(): max_attempts = await get_anti_truncation_max_attempts() - # 使用 apply_anti_truncation_to_stream 包装请求 - # 这个函数会自动处理所有的续传逻辑 - streaming_response = await apply_anti_truncation_to_stream( - non_stream_request, - api_request, + # 首先对payload应用反截断指令 + anti_truncation_payload = apply_anti_truncation(api_request) + + # 定义流式请求函数(返回 StreamingResponse) + async def stream_request_wrapper(payload): + # stream_request 返回异步生成器,需要包装成 StreamingResponse + stream_gen = stream_request(body=payload, native=False) + return StreamingResponse(stream_gen, media_type="text/event-stream") + + # 创建反截断处理器 + processor = AntiTruncationStreamProcessor( + stream_request_wrapper, + anti_truncation_payload, max_attempts ) - # yield StreamingResponse 的内容 - async for chunk in streaming_response.body_iterator: + # 直接迭代 process_stream() 生成器 + async for chunk in processor.process_stream(): yield chunk # ========== 普通流式生成器 ========== diff --git a/src/router/geminicli/openai.py b/src/router/geminicli/openai.py index 97956ce29..df4fa1895 100644 --- a/src/router/geminicli/openai.py +++ b/src/router/geminicli/openai.py @@ -274,16 +274,25 @@ async def get_response(): # ========== 流式抗截断生成器 ========== async def anti_truncation_generator(): - from src.converter.anti_truncation import apply_anti_truncation_to_stream - from src.api.geminicli import non_stream_request + from src.converter.anti_truncation import AntiTruncationStreamProcessor + from src.api.geminicli import stream_request + from src.converter.anti_truncation import apply_anti_truncation max_attempts = await get_anti_truncation_max_attempts() - # 使用 apply_anti_truncation_to_stream 包装请求 - # 这个函数会自动处理所有的续传逻辑 - streaming_response = await apply_anti_truncation_to_stream( - non_stream_request, - api_request, + # 首先对payload应用反截断指令 + anti_truncation_payload = apply_anti_truncation(api_request) + + # 定义流式请求函数(返回 StreamingResponse) + async def stream_request_wrapper(payload): + # stream_request 返回异步生成器,需要包装成 StreamingResponse + stream_gen = stream_request(body=payload, native=False) + return StreamingResponse(stream_gen, media_type="text/event-stream") + + # 创建反截断处理器 + processor = AntiTruncationStreamProcessor( + stream_request_wrapper, + anti_truncation_payload, max_attempts ) @@ -291,18 +300,23 @@ async def anti_truncation_generator(): import uuid response_id = str(uuid.uuid4()) - # yield StreamingResponse 的内容,并转换为 OpenAI 格式 - async for chunk in streaming_response.body_iterator: + # 直接迭代 process_stream() 生成器,并转换为 OpenAI 格式 + async for chunk in processor.process_stream(): if not chunk: continue # 解析 Gemini SSE 格式 chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk - # 跳过空行和 [DONE] 标记 - if not chunk_str.strip() or chunk_str.strip() == "data: [DONE]": + # 跳过空行 + if not chunk_str.strip(): continue + # 处理 [DONE] 标记 + if chunk_str.strip() == "data: [DONE]": + yield "data: [DONE]\n\n".encode('utf-8') + return + # 解析 "data: {...}" 格式 if chunk_str.startswith("data: "): try: @@ -322,7 +336,7 @@ async def anti_truncation_generator(): continue # 发送结束标记 - yield "data: [DONE]\n\n".encode() + yield "data: [DONE]\n\n".encode('utf-8') # ========== 普通流式生成器 ========== async def normal_stream_generator(): From 9148d42a7fae739f5777a0edbc097427893a5e6a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 17:50:54 +0000 Subject: [PATCH 025/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 387671b13..8dc06eb1f 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=be9c97d4575f04ef2a8a6201a706272761c1cf6a -short_hash=be9c97d -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 01:31:02 +0800 +full_hash=a1388e17bb9c1b4c6eaf831b1f704a2ec5a1045d +short_hash=a1388e1 +message=修复流式抗截断模型 +date=2026-01-11 01:50:45 +0800 From 3a5fc86b28844736a2b613929abc254e6431e286 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 02:01:03 +0800 Subject: [PATCH 026/211] Update anti_truncation.py --- src/converter/anti_truncation.py | 68 ++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/src/converter/anti_truncation.py b/src/converter/anti_truncation.py index 14b2da105..7b3a3a64d 100644 --- a/src/converter/anti_truncation.py +++ b/src/converter/anti_truncation.py @@ -409,6 +409,10 @@ def _extract_content_from_chunk(self, data: Dict[str, Any]) -> str: """从chunk数据中提取文本内容""" content = "" + # 先尝试解包 response 字段(Gemini API 格式) + if "response" in data: + data = data["response"] + # 处理 Gemini 格式 if "candidates" in data: for candidate in data["candidates"]: @@ -528,6 +532,10 @@ def _extract_content_from_response(self, data: Dict[str, Any]) -> str: """从响应数据中提取文本内容""" content = "" + # 先尝试解包 response 字段(Gemini API 格式) + if "response" in data: + data = data["response"] + # 处理Gemini格式 if "candidates" in data: for candidate in data["candidates"]: @@ -552,18 +560,35 @@ def _remove_done_marker_from_line(self, line: bytes, line_str: str, data: Dict[s if "[done]" not in line_str.lower(): return line # 没有[done]标记,直接返回原始行 + log.info(f"Anti-truncation: Attempting to remove [done] marker from line") + log.debug(f"Anti-truncation: Original line (first 200 chars): {line_str[:200]}") + # 编译正则表达式,匹配[done]标记(忽略大小写,包括可能的空白字符) done_pattern = re.compile(r"\s*\[done\]\s*", re.IGNORECASE) + # 检查是否有 response 包裹层 + has_response_wrapper = "response" in data + log.debug(f"Anti-truncation: has_response_wrapper={has_response_wrapper}, data keys={list(data.keys())}") + if has_response_wrapper: + # 需要保留外层的 response 字段 + inner_data = data["response"] + else: + inner_data = data + + log.debug(f"Anti-truncation: inner_data keys={list(inner_data.keys())}") + + log.debug(f"Anti-truncation: inner_data keys={list(inner_data.keys())}") + # 处理Gemini格式 - if "candidates" in data: - modified_data = data.copy() - modified_data["candidates"] = [] + if "candidates" in inner_data: + log.info(f"Anti-truncation: Processing Gemini format to remove [done] marker") + modified_inner = inner_data.copy() + modified_inner["candidates"] = [] - for i, candidate in enumerate(data["candidates"]): + for i, candidate in enumerate(inner_data["candidates"]): modified_candidate = candidate.copy() # 只在最后一个candidate中清理[done]标记 - is_last_candidate = i == len(data["candidates"]) - 1 + is_last_candidate = i == len(inner_data["candidates"]) - 1 if "content" in candidate: modified_content = candidate["content"].copy() @@ -572,26 +597,38 @@ def _remove_done_marker_from_line(self, line: bytes, line_str: str, data: Dict[s for part in modified_content["parts"]: if "text" in part and isinstance(part["text"], str): modified_part = part.copy() + original_text = part["text"] # 只在最后一个candidate中清理[done]标记 if is_last_candidate: modified_part["text"] = done_pattern.sub("", part["text"]) + if "[done]" in original_text.lower(): + log.debug(f"Anti-truncation: Removed [done] from text: '{original_text[:100]}' -> '{modified_part['text'][:100]}'") modified_parts.append(modified_part) else: modified_parts.append(part) modified_content["parts"] = modified_parts modified_candidate["content"] = modified_content - modified_data["candidates"].append(modified_candidate) + modified_inner["candidates"].append(modified_candidate) + + # 如果有 response 包裹层,需要重新包装 + if has_response_wrapper: + modified_data = data.copy() + modified_data["response"] = modified_inner + else: + modified_data = modified_inner # 重新编码为行格式 - SSE格式需要两个换行符 json_str = json.dumps(modified_data, separators=(",", ":"), ensure_ascii=False) - return f"data: {json_str}\n\n".encode("utf-8") + result = f"data: {json_str}\n\n".encode("utf-8") + log.debug(f"Anti-truncation: Modified line (first 200 chars): {result.decode('utf-8', errors='ignore')[:200]}") + return result # 处理OpenAI格式 - elif "choices" in data: - modified_data = data.copy() - modified_data["choices"] = [] + elif "choices" in inner_data: + modified_inner = inner_data.copy() + modified_inner["choices"] = [] - for choice in data["choices"]: + for choice in inner_data["choices"]: modified_choice = choice.copy() if "delta" in choice and "content" in choice["delta"]: modified_delta = choice["delta"].copy() @@ -601,7 +638,14 @@ def _remove_done_marker_from_line(self, line: bytes, line_str: str, data: Dict[s modified_message = choice["message"].copy() modified_message["content"] = done_pattern.sub("", choice["message"]["content"]) modified_choice["message"] = modified_message - modified_data["choices"].append(modified_choice) + modified_inner["choices"].append(modified_choice) + + # 如果有 response 包裹层,需要重新包装 + if has_response_wrapper: + modified_data = data.copy() + modified_data["response"] = modified_inner + else: + modified_data = modified_inner # 重新编码为行格式 - SSE格式需要两个换行符 json_str = json.dumps(modified_data, separators=(",", ":"), ensure_ascii=False) From 490947e984c2bf5689ff94118324d08615f13e93 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 02:07:00 +0800 Subject: [PATCH 027/211] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dgemini=E7=9A=84?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E6=8A=97=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anti_truncation.py | 2 +- src/router/antigravity/gemini.py | 37 ++++++++++++++++++++++++++++++-- src/router/geminicli/gemini.py | 37 ++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/converter/anti_truncation.py b/src/converter/anti_truncation.py index 7b3a3a64d..5f7c78db3 100644 --- a/src/converter/anti_truncation.py +++ b/src/converter/anti_truncation.py @@ -285,7 +285,7 @@ async def process_stream(self) -> AsyncGenerator[bytes, None]: log.debug(f"Anti-truncation: Check done marker result: {has_marker}, DONE_MARKER='{DONE_MARKER}'") if has_marker: found_done_marker = True - log.info(f"Anti-truncation: Found [done] marker in chunk, content: {content[:200]}") + log.debug(f"Anti-truncation: Found [done] marker in chunk, content: {content[:200]}") # 清理行中的[done]标记后再发送 cleaned_line = self._remove_done_marker_from_line(line, line_str, data) diff --git a/src/router/antigravity/gemini.py b/src/router/antigravity/gemini.py index a09417fe6..4640f4dc3 100644 --- a/src/router/antigravity/gemini.py +++ b/src/router/antigravity/gemini.py @@ -284,9 +284,42 @@ async def stream_request_wrapper(payload): max_attempts ) - # 直接迭代 process_stream() 生成器 + # 迭代 process_stream() 生成器,并展开 response 包装 async for chunk in processor.process_stream(): - yield chunk + if isinstance(chunk, (str, bytes)): + chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk + + # 解析并展开 response 包装 + if chunk_str.startswith("data: "): + json_str = chunk_str[6:].strip() + + # 跳过 [DONE] 标记 + if json_str == "[DONE]": + yield chunk + continue + + try: + # 解析JSON + data = json.loads(json_str) + + # 展开 response 包装 + if "response" in data and "candidates" not in data: + log.debug(f"[ANTIGRAVITY-ANTI-TRUNCATION] 展开response包装") + unwrapped_data = data["response"] + # 重新构建SSE格式 + yield f"data: {json.dumps(unwrapped_data, ensure_ascii=False)}\n\n".encode('utf-8') + else: + # 已经是展开的格式,直接返回 + yield chunk + except json.JSONDecodeError: + # JSON解析失败,直接返回原始chunk + yield chunk + else: + # 不是SSE格式,直接返回 + yield chunk + else: + # 其他类型,直接返回 + yield chunk # ========== 普通流式生成器 ========== async def normal_stream_generator(): diff --git a/src/router/geminicli/gemini.py b/src/router/geminicli/gemini.py index b40220e65..f1d889101 100644 --- a/src/router/geminicli/gemini.py +++ b/src/router/geminicli/gemini.py @@ -283,9 +283,42 @@ async def stream_request_wrapper(payload): max_attempts ) - # 直接迭代 process_stream() 生成器 + # 迭代 process_stream() 生成器,并展开 response 包装 async for chunk in processor.process_stream(): - yield chunk + if isinstance(chunk, (str, bytes)): + chunk_str = chunk.decode('utf-8') if isinstance(chunk, bytes) else chunk + + # 解析并展开 response 包装 + if chunk_str.startswith("data: "): + json_str = chunk_str[6:].strip() + + # 跳过 [DONE] 标记 + if json_str == "[DONE]": + yield chunk + continue + + try: + # 解析JSON + data = json.loads(json_str) + + # 展开 response 包装 + if "response" in data and "candidates" not in data: + log.debug(f"[GEMINICLI-ANTI-TRUNCATION] 展开response包装") + unwrapped_data = data["response"] + # 重新构建SSE格式 + yield f"data: {json.dumps(unwrapped_data, ensure_ascii=False)}\n\n".encode('utf-8') + else: + # 已经是展开的格式,直接返回 + yield chunk + except json.JSONDecodeError: + # JSON解析失败,直接返回原始chunk + yield chunk + else: + # 不是SSE格式,直接返回 + yield chunk + else: + # 其他类型,直接返回 + yield chunk # ========== 普通流式生成器 ========== async def normal_stream_generator(): From cdcd88812d88faf4b1b4502fed88ced3f6f05f5f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jan 2026 18:07:09 +0000 Subject: [PATCH 028/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 8dc06eb1f..5426e7d38 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=a1388e17bb9c1b4c6eaf831b1f704a2ec5a1045d -short_hash=a1388e1 -message=修复流式抗截断模型 -date=2026-01-11 01:50:45 +0800 +full_hash=490947e984c2bf5689ff94118324d08615f13e93 +short_hash=490947e +message=修复gemini的流式抗截断 +date=2026-01-11 02:07:00 +0800 From 57909911448ea2cbdc763643ff56f5523d6fe520 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 09:44:58 +0800 Subject: [PATCH 029/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 0f5ef4b09..0ecf8a2a0 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -216,7 +216,7 @@ async def normalize_gemini_request( result = request.copy() model = result.get("model", "") - generation_config = result.get("generationConfig", {}).copy() # 创建副本避免修改原对象 + generation_config = (result.get("generationConfig") or {}).copy() # 创建副本避免修改原对象 tools = result.get("tools") system_instruction = result.get("systemInstruction") or result.get("system_instructions") From 582e27f5ab917e91cebf47cfbcfbae9b4557a02b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 01:45:37 +0000 Subject: [PATCH 030/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 5426e7d38..e5afc80a1 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=490947e984c2bf5689ff94118324d08615f13e93 -short_hash=490947e -message=修复gemini的流式抗截断 -date=2026-01-11 02:07:00 +0800 +full_hash=a9ffe3a43245ee490d1b9e377e573fe80b14a653 +short_hash=a9ffe3a +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-11 09:45:22 +0800 From c1b17c05754f0934cbec807b399648a2e8baa0e9 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 09:55:44 +0800 Subject: [PATCH 031/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 0ecf8a2a0..7e79e12ad 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -244,8 +244,9 @@ async def normalize_gemini_request( # 3. 搜索模型添加 Google Search if is_search_model(model): - result_tools = result.setdefault("tools", []) - if not any(tool.get("googleSearch") for tool in result_tools): + result_tools = result.get("tools") or [] + result["tools"] = result_tools + if not any(tool.get("googleSearch") for tool in result_tools if isinstance(tool, dict)): result_tools.append({"googleSearch": {}}) # 4. 模型名称处理 From 160ba33f4100735ca5ac84ca12b9ca73c9f6aafb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 01:56:37 +0000 Subject: [PATCH 032/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index e5afc80a1..60d4f519b 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=a9ffe3a43245ee490d1b9e377e573fe80b14a653 -short_hash=a9ffe3a +full_hash=4390209c8d9b9752ace976251af288e9f4b9a19a +short_hash=4390209 message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 09:45:22 +0800 +date=2026-01-11 09:56:25 +0800 From 8c0b24867640e6dc38cf2a462822bd00435598b3 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 10:16:34 +0800 Subject: [PATCH 033/211] Update utils.py --- src/converter/utils.py | 68 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/src/converter/utils.py b/src/converter/utils.py index e00108beb..99469ea07 100644 --- a/src/converter/utils.py +++ b/src/converter/utils.py @@ -99,9 +99,66 @@ async def merge_system_messages(request_body: Dict[str, Any]) -> Dict[str, Any]: {"role": "user", "content": "Hello"} ] } + + Example (Anthropic格式,兼容性模式关闭): + 输入: + { + "system": "You are a helpful assistant.", + "messages": [ + {"role": "user", "content": "Hello"} + ] + } + + 输出: + { + "systemInstruction": { + "parts": [ + {"text": "You are a helpful assistant."} + ] + }, + "messages": [ + {"role": "user", "content": "Hello"} + ] + } """ from config import get_compatibility_mode_enabled + compatibility_mode = await get_compatibility_mode_enabled() + + # 处理 Anthropic 格式的顶层 system 参数 + # Anthropic API 规范: system 是顶层参数,不在 messages 中 + system_content = request_body.get("system") + if system_content and "systemInstruction" not in request_body: + system_parts = [] + + if isinstance(system_content, str): + if system_content.strip(): + system_parts.append({"text": system_content}) + elif isinstance(system_content, list): + # system 可以是包含多个块的列表 + for item in system_content: + if isinstance(item, dict): + if item.get("type") == "text" and item.get("text", "").strip(): + system_parts.append({"text": item["text"]}) + elif isinstance(item, str) and item.strip(): + system_parts.append({"text": item}) + + if system_parts: + if compatibility_mode: + # 兼容性模式:将 system 转换为 user 消息插入到 messages 开头 + user_system_message = { + "role": "user", + "content": system_content if isinstance(system_content, str) else + "\n".join(part["text"] for part in system_parts) + } + messages = request_body.get("messages", []) + request_body = request_body.copy() + request_body["messages"] = [user_system_message] + messages + else: + # 非兼容性模式:添加为 systemInstruction + request_body = request_body.copy() + request_body["systemInstruction"] = {"parts": system_parts} + messages = request_body.get("messages", []) if not messages: return request_body @@ -126,6 +183,13 @@ async def merge_system_messages(request_body: Dict[str, Any]) -> Dict[str, Any]: else: # 兼容性模式关闭:提取连续的system消息合并为systemInstruction system_parts = [] + + # 如果已经从顶层 system 参数创建了 systemInstruction,获取现有的 parts + if "systemInstruction" in request_body: + existing_instruction = request_body.get("systemInstruction", {}) + if isinstance(existing_instruction, dict): + system_parts = existing_instruction.get("parts", []).copy() + remaining_messages = [] collecting_system = True @@ -151,14 +215,14 @@ async def merge_system_messages(request_body: Dict[str, Any]) -> Dict[str, Any]: collecting_system = False remaining_messages.append(message) - # 如果没有找到system消息,返回原始请求体 + # 如果没有找到任何system消息(包括顶层参数和messages中的),返回原始请求体 if not system_parts: return request_body # 构建新的请求体 result = request_body.copy() - # 添加systemInstruction + # 添加或更新systemInstruction result["systemInstruction"] = {"parts": system_parts} # 更新messages列表(移除已处理的system消息) From 427e42f1e53144c358a724b4d684a491d8b74f1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 02:17:06 +0000 Subject: [PATCH 034/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 60d4f519b..238acc3ee 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=4390209c8d9b9752ace976251af288e9f4b9a19a -short_hash=4390209 +full_hash=51045cbd5f6303d16be9f11adb5113ae94d7c5dd +short_hash=51045cb message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 09:56:25 +0800 +date=2026-01-11 10:16:51 +0800 From 74f759cad39f34efb7189cbecb7e41c9f9b27f84 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 10:43:39 +0800 Subject: [PATCH 035/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 7e79e12ad..a49338237 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -275,10 +275,15 @@ async def normalize_gemini_request( # 3. 思考模型处理 if is_thinking_model(model): if "thinkingConfig" not in generation_config: - generation_config["thinkingConfig"] = { - "thinkingBudget": 1024, - "includeThoughts": return_thoughts - } + generation_config["thinkingConfig"] = {} + + thinking_config = generation_config["thinkingConfig"] + # 优先使用传入的思考预算,否则使用默认值 + if "thinkingBudget" not in thinking_config: + thinking_config["thinkingBudget"] = 1024 + if "includeThoughts" not in thinking_config: + thinking_config["includeThoughts"] = return_thoughts + # 移除 -thinking 后缀 model = model.replace("-thinking", "") From 414abd16b64be8dfa2988b38cab022463a39a166 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 02:43:49 +0000 Subject: [PATCH 036/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 238acc3ee..aa1d4c5b2 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=51045cbd5f6303d16be9f11adb5113ae94d7c5dd -short_hash=51045cb -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 10:16:51 +0800 +full_hash=74f759cad39f34efb7189cbecb7e41c9f9b27f84 +short_hash=74f759c +message=Update gemini_fix.py +date=2026-01-11 10:43:39 +0800 From 0a656a788effb09555466527c63f15f8a05a1b9b Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 11:05:52 +0800 Subject: [PATCH 037/211] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=9A=84=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 35 +++++++++-------- src/converter/gemini_fix.py | 64 +++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index e0ec843ff..d757f16ff 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -252,37 +252,40 @@ def convert_messages_to_contents( if not include_thinking: continue - signature = item.get("signature") - if not signature: - continue - thinking_text = item.get("thinking", "") if thinking_text is None: thinking_text = "" + part: Dict[str, Any] = { "text": str(thinking_text), "thought": True, - "thoughtSignature": signature, } + + # 如果有 signature 则添加 + signature = item.get("signature") + if signature: + part["thoughtSignature"] = signature + parts.append(part) elif item_type == "redacted_thinking": if not include_thinking: continue - signature = item.get("signature") - if not signature: - continue - thinking_text = item.get("thinking") if thinking_text is None: thinking_text = item.get("data", "") - parts.append( - { - "text": str(thinking_text or ""), - "thought": True, - "thoughtSignature": signature, - } - ) + + part_dict: Dict[str, Any] = { + "text": str(thinking_text or ""), + "thought": True, + } + + # 如果有 signature 则添加 + signature = item.get("signature") + if signature: + part_dict["thoughtSignature"] = signature + + parts.append(part_dict) elif item_type == "text": text = item.get("text", "") if _is_non_whitespace_text(text): diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index a49338237..482c2e7f0 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -191,6 +191,44 @@ def is_thinking_model(model_name: str) -> bool: return "-thinking" in model_name or "pro" in model_name.lower() +def check_last_assistant_has_thinking(contents: List[Dict[str, Any]]) -> bool: + """ + 检查最后一个 assistant 消息是否以 thinking 块开始 + + 根据 Claude API 要求:当启用 thinking 时,最后一个 assistant 消息必须以 thinking 块开始 + + Args: + contents: Gemini 格式的 contents 数组 + + Returns: + 如果最后一个 assistant 消息以 thinking 块开始则返回 True,否则返回 False + """ + if not contents: + return True # 没有 contents,允许启用 thinking + + # 从后往前找最后一个 assistant (model) 消息 + last_assistant_content = None + for content in reversed(contents): + if isinstance(content, dict) and content.get("role") == "model": + last_assistant_content = content + break + + if not last_assistant_content: + return True # 没有 assistant 消息,允许启用 thinking + + # 检查第一个 part 是否是 thinking 块 + parts = last_assistant_content.get("parts", []) + if not parts: + return False # 有 assistant 消息但没有 parts,不允许 thinking + + first_part = parts[0] + if not isinstance(first_part, dict): + return False + + # 检查是否是 thinking 块(有 thought 字段且为 True) + return first_part.get("thought") is True + + async def normalize_gemini_request( request: Dict[str, Any], mode: str = "geminicli" @@ -274,15 +312,25 @@ async def normalize_gemini_request( else: # 3. 思考模型处理 if is_thinking_model(model): - if "thinkingConfig" not in generation_config: - generation_config["thinkingConfig"] = {} + # 检查最后一个 assistant 消息是否以 thinking 块开始 + contents = result.get("contents", []) + can_enable_thinking = check_last_assistant_has_thinking(contents) - thinking_config = generation_config["thinkingConfig"] - # 优先使用传入的思考预算,否则使用默认值 - if "thinkingBudget" not in thinking_config: - thinking_config["thinkingBudget"] = 1024 - if "includeThoughts" not in thinking_config: - thinking_config["includeThoughts"] = return_thoughts + if can_enable_thinking: + if "thinkingConfig" not in generation_config: + generation_config["thinkingConfig"] = {} + + thinking_config = generation_config["thinkingConfig"] + # 优先使用传入的思考预算,否则使用默认值 + if "thinkingBudget" not in thinking_config: + thinking_config["thinkingBudget"] = 1024 + if "includeThoughts" not in thinking_config: + thinking_config["includeThoughts"] = return_thoughts + else: + # 最后一个 assistant 消息不是以 thinking 块开始,禁用 thinking + log.warning(f"[ANTIGRAVITY] 最后一个 assistant 消息不以 thinking 块开始,禁用 thinkingConfig") + # 移除可能存在的 thinkingConfig + generation_config.pop("thinkingConfig", None) # 移除 -thinking 后缀 model = model.replace("-thinking", "") From 010b33c22288740bb28c76b84056e2a0078c49e4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 03:06:17 +0000 Subject: [PATCH 038/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index aa1d4c5b2..b706ae330 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=74f759cad39f34efb7189cbecb7e41c9f9b27f84 -short_hash=74f759c -message=Update gemini_fix.py -date=2026-01-11 10:43:39 +0800 +full_hash=18e2484001889f83d3284e1d80dde83496c55885 +short_hash=18e2484 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-11 11:06:06 +0800 From d5668fe3b310dddad053307670ae8094641b5e93 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 11:11:12 +0800 Subject: [PATCH 039/211] =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E5=AD=97?= =?UTF-8?q?=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/gemini_fix.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 482c2e7f0..ccb2f2429 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -369,25 +369,36 @@ async def normalize_gemini_request( if tools: result["tools"] = clean_tools_for_gemini(tools) - # 4. 清理空的 parts(修复 400 错误:required oneof field 'data' must have one initialized field) + # 4. 清理空的 parts 和未知字段(修复 400 错误:required oneof field 'data' must have one initialized field) + # 同时移除不支持的字段如 cache_control if "contents" in result: + # 定义 part 中允许的字段集合 + ALLOWED_PART_KEYS = { + "text", "inlineData", "fileData", "functionCall", "functionResponse", + "thought", "thoughtSignature" # thinking 相关字段 + } + cleaned_contents = [] for content in result["contents"]: if isinstance(content, dict) and "parts" in content: - # 过滤掉空的或无效的 parts + # 过滤掉空的或无效的 parts,并移除未知字段 valid_parts = [] for part in content["parts"]: if not isinstance(part, dict): continue + + # 移除不支持的字段(如 cache_control) + cleaned_part = {k: v for k, v in part.items() if k in ALLOWED_PART_KEYS} + # 检查 part 是否有有效的数据字段 has_valid_data = any( - key in part and part[key] + key in cleaned_part and cleaned_part[key] for key in ["text", "inlineData", "fileData", "functionCall", "functionResponse"] ) if has_valid_data: - valid_parts.append(part) + valid_parts.append(cleaned_part) else: - log.warning(f"[GEMINI_FIX] 移除空的 part: {part}") + log.warning(f"[GEMINI_FIX] 移除空的或无效的 part: {part}") # 只添加有有效 parts 的 content if valid_parts: From c0e1f13e8ae575cbbe56a5547eaaab661b440e6c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 03:11:37 +0000 Subject: [PATCH 040/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index b706ae330..b43d38ec6 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=18e2484001889f83d3284e1d80dde83496c55885 -short_hash=18e2484 +full_hash=1dbd7bfb11d5216f19733f3ad918ed27ef5c015d +short_hash=1dbd7bf message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 11:06:06 +0800 +date=2026-01-11 11:11:24 +0800 From 534c2df503c194608bf8f3e8cfe9d40c84357180 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 11:26:22 +0800 Subject: [PATCH 041/211] Update openai2gemini.py --- src/converter/openai2gemini.py | 95 +++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index c7fcad49a..a033725f5 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -227,6 +227,84 @@ def _clean_schema_for_gemini(schema: Any) -> Any: return cleaned +def fix_tool_call_args_types( + args: Dict[str, Any], + parameters_schema: Dict[str, Any] +) -> Dict[str, Any]: + """ + 根据工具的参数 schema 修正函数调用参数的类型 + + 例如:将字符串 "5" 转换为数字 5,根据 schema 中的 type 定义 + + Args: + args: 函数调用的参数字典 + parameters_schema: 工具定义中的 parameters schema + + Returns: + 类型修正后的参数字典 + """ + if not args or not parameters_schema: + return args + + properties = parameters_schema.get("properties", {}) + if not properties: + return args + + fixed_args = {} + for key, value in args.items(): + if key not in properties: + # 参数不在 schema 中,保持原样 + fixed_args[key] = value + continue + + param_schema = properties[key] + param_type = param_schema.get("type") + + # 根据 schema 中的类型修正参数值 + if param_type == "number" or param_type == "integer": + # 如果值是字符串,尝试转换为数字 + if isinstance(value, str): + try: + if param_type == "integer": + fixed_args[key] = int(value) + else: + # 尝试转换为 float,如果是整数则保持为 int + num_value = float(value) + fixed_args[key] = int(num_value) if num_value.is_integer() else num_value + log.debug(f"[OPENAI2GEMINI] 修正参数类型: {key} '{value}' -> {fixed_args[key]} ({param_type})") + except (ValueError, AttributeError): + # 转换失败,保持原样 + fixed_args[key] = value + log.warning(f"[OPENAI2GEMINI] 无法将参数 {key} 的值 '{value}' 转换为 {param_type}") + else: + fixed_args[key] = value + elif param_type == "boolean": + # 如果值是字符串,转换为布尔值 + if isinstance(value, str): + if value.lower() in ("true", "1", "yes"): + fixed_args[key] = True + elif value.lower() in ("false", "0", "no"): + fixed_args[key] = False + else: + fixed_args[key] = value + if fixed_args[key] != value: + log.debug(f"[OPENAI2GEMINI] 修正参数类型: {key} '{value}' -> {fixed_args[key]} (boolean)") + else: + fixed_args[key] = value + elif param_type == "string": + # 如果值不是字符串,转换为字符串 + if not isinstance(value, str): + fixed_args[key] = str(value) + log.debug(f"[OPENAI2GEMINI] 修正参数类型: {key} {value} -> '{fixed_args[key]}' (string)") + else: + fixed_args[key] = value + else: + # 其他类型(array, object 等)保持原样 + fixed_args[key] = value + + return fixed_args + + def convert_openai_tools_to_gemini(openai_tools: List) -> List[Dict[str, Any]]: """ 将 OpenAI tools 格式转换为 Gemini functionDeclarations 格式 @@ -478,6 +556,16 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di # 提取消息列表 messages = openai_request.get("messages", []) + + # 构建工具名称到参数 schema 的映射(用于类型修正) + tool_schemas = {} + if "tools" in openai_request and openai_request["tools"]: + for tool in openai_request["tools"]: + if tool.get("type") == "function": + function = tool.get("function", {}) + func_name = function.get("name") + if func_name: + tool_schemas[func_name] = function.get("parameters", {}) for message in messages: role = message.get("role", "user") @@ -545,6 +633,11 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di if isinstance(tool_call["function"]["arguments"], str) else tool_call["function"]["arguments"] ) + + # 根据工具的 schema 修正参数类型 + func_name = tool_call["function"]["name"] + if func_name in tool_schemas: + args = fix_tool_call_args_types(args, tool_schemas[func_name]) # 解码工具ID和thoughtSignature encoded_id = tool_call.get("id", "") @@ -554,7 +647,7 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di function_call_part = { "functionCall": { "id": original_id, - "name": tool_call["function"]["name"], + "name": func_name, "args": args } } From 8928d04f008318b790d2e431cfd53279677b297e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 03:26:46 +0000 Subject: [PATCH 042/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index b43d38ec6..664d2e89a 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=1dbd7bfb11d5216f19733f3ad918ed27ef5c015d -short_hash=1dbd7bf +full_hash=196ab8136ee7b8dcf4b7b8e82ff1733a3f5db0f2 +short_hash=196ab81 message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 11:11:24 +0800 +date=2026-01-11 11:26:34 +0800 From d62e68ff096596ff021ac0ffce7384afa3158ff3 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 11:54:58 +0800 Subject: [PATCH 043/211] Update utils.py --- src/converter/utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/converter/utils.py b/src/converter/utils.py index 99469ea07..2d313adc0 100644 --- a/src/converter/utils.py +++ b/src/converter/utils.py @@ -128,7 +128,7 @@ async def merge_system_messages(request_body: Dict[str, Any]) -> Dict[str, Any]: # 处理 Anthropic 格式的顶层 system 参数 # Anthropic API 规范: system 是顶层参数,不在 messages 中 system_content = request_body.get("system") - if system_content and "systemInstruction" not in request_body: + if system_content: system_parts = [] if isinstance(system_content, str): @@ -213,7 +213,13 @@ async def merge_system_messages(request_body: Dict[str, Any]) -> Dict[str, Any]: else: # 遇到非system消息,停止收集 collecting_system = False - remaining_messages.append(message) + if role == "system": + # 将后续的system消息转换为user消息 + converted_message = message.copy() + converted_message["role"] = "user" + remaining_messages.append(converted_message) + else: + remaining_messages.append(message) # 如果没有找到任何system消息(包括顶层参数和messages中的),返回原始请求体 if not system_parts: From 9d6b2949dfb6ccbd44f26505ea81b12b303057f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 03:55:19 +0000 Subject: [PATCH 044/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 664d2e89a..952521cea 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=196ab8136ee7b8dcf4b7b8e82ff1733a3f5db0f2 -short_hash=196ab81 +full_hash=7e8afa2a0d68405134abe3e90b5a756b2de51bf4 +short_hash=7e8afa2 message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 11:26:34 +0800 +date=2026-01-11 11:55:08 +0800 From ea6b6f4f2045db2ce7784693a77f1e3e89d3f8bc Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 16:56:27 +0800 Subject: [PATCH 045/211] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 243 +++++++++++++- src/converter/gemini_fix.py | 17 +- src/converter/openai2gemini.py | 517 ++++++++++++++++++++++++------ 3 files changed, 646 insertions(+), 131 deletions(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index d757f16ff..d4004b347 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -21,6 +21,166 @@ DEFAULT_TEMPERATURE = 0.4 _DEBUG_TRUE = {"1", "true", "yes", "on"} +# ============================================================================ +# Thinking 块验证和清理 +# ============================================================================ + +# 最小有效签名长度 +MIN_SIGNATURE_LENGTH = 10 + + +def has_valid_signature(block: Dict[str, Any]) -> bool: + """ + 检查 thinking 块是否有有效签名 + + Args: + block: content block 字典 + + Returns: + bool: 是否有有效签名 + """ + if not isinstance(block, dict): + return True + + block_type = block.get("type") + if block_type not in ("thinking", "redacted_thinking"): + return True # 非 thinking 块默认有效 + + thinking = block.get("thinking", "") + signature = block.get("signature") + + # 空 thinking + 任意 signature = 有效 (trailing signature case) + if not thinking and signature is not None: + return True + + # 有内容 + 足够长度的 signature = 有效 + if signature and isinstance(signature, str) and len(signature) >= MIN_SIGNATURE_LENGTH: + return True + + return False + + +def sanitize_thinking_block(block: Dict[str, Any]) -> Dict[str, Any]: + """ + 清理 thinking 块,只保留必要字段(移除 cache_control 等) + + Args: + block: content block 字典 + + Returns: + 清理后的 block 字典 + """ + if not isinstance(block, dict): + return block + + block_type = block.get("type") + if block_type not in ("thinking", "redacted_thinking"): + return block + + # 重建块,移除额外字段 + sanitized: Dict[str, Any] = { + "type": block_type, + "thinking": block.get("thinking", "") + } + + signature = block.get("signature") + if signature: + sanitized["signature"] = signature + + return sanitized + + +def remove_trailing_unsigned_thinking(blocks: List[Dict[str, Any]]) -> None: + """ + 移除尾部的无签名 thinking 块 + + Args: + blocks: content blocks 列表 (会被修改) + """ + if not blocks: + return + + # 从后向前扫描 + end_index = len(blocks) + for i in range(len(blocks) - 1, -1, -1): + block = blocks[i] + if not isinstance(block, dict): + break + + block_type = block.get("type") + if block_type in ("thinking", "redacted_thinking"): + if not has_valid_signature(block): + end_index = i + else: + break # 遇到有效签名的 thinking 块,停止 + else: + break # 遇到非 thinking 块,停止 + + if end_index < len(blocks): + removed = len(blocks) - end_index + del blocks[end_index:] + log.debug(f"Removed {removed} trailing unsigned thinking block(s)") + + +def filter_invalid_thinking_blocks(messages: List[Dict[str, Any]]) -> None: + """ + 过滤消息中的无效 thinking 块 + + Args: + messages: Anthropic messages 列表 (会被修改) + """ + total_filtered = 0 + + for msg in messages: + # 只处理 assistant 和 model 消息 + role = msg.get("role", "") + if role not in ("assistant", "model"): + continue + + content = msg.get("content") + if not isinstance(content, list): + continue + + original_len = len(content) + new_blocks: List[Dict[str, Any]] = [] + + for block in content: + if not isinstance(block, dict): + new_blocks.append(block) + continue + + block_type = block.get("type") + if block_type not in ("thinking", "redacted_thinking"): + new_blocks.append(block) + continue + + # 检查 thinking 块的有效性 + if has_valid_signature(block): + # 有效签名,清理后保留 + new_blocks.append(sanitize_thinking_block(block)) + else: + # 无效签名,将内容转换为 text 块 + thinking_text = block.get("thinking", "") + if thinking_text and str(thinking_text).strip(): + log.info( + f"[Claude-Handler] Converting thinking block with invalid signature to text. " + f"Content length: {len(thinking_text)} chars" + ) + new_blocks.append({"type": "text", "text": thinking_text}) + else: + log.debug("[Claude-Handler] Dropping empty thinking block with invalid signature") + + msg["content"] = new_blocks + filtered_count = original_len - len(new_blocks) + total_filtered += filtered_count + + # 如果过滤后为空,添加一个空文本块以保持消息有效 + if not new_blocks: + msg["content"] = [{"type": "text", "text": ""}] + + if total_filtered > 0: + log.debug(f"Filtered {total_filtered} invalid thinking block(s) from history") + # ============================================================================ # 请求验证和提取 @@ -214,17 +374,21 @@ def convert_messages_to_contents( """ contents: List[Dict[str, Any]] = [] - # 第一遍:构建 tool_use_id -> name 的映射 - tool_use_names: Dict[str, str] = {} + # 第一遍:构建 tool_use_id -> (name, signature) 的映射 + # 注意:存储的是编码后的 ID(可能包含签名) + tool_use_info: Dict[str, tuple[str, Optional[str]]] = {} for msg in messages: raw_content = msg.get("content", "") if isinstance(raw_content, list): for item in raw_content: if isinstance(item, dict) and item.get("type") == "tool_use": - tool_id = item.get("id") + encoded_tool_id = item.get("id") tool_name = item.get("name") - if tool_id and tool_name: - tool_use_names[str(tool_id)] = tool_name + if encoded_tool_id and tool_name: + # 解码获取原始ID和签名 + original_id, signature = decode_tool_id_and_signature(encoded_tool_id) + # 存储映射:编码ID -> (name, signature) + tool_use_info[str(encoded_tool_id)] = (tool_name, signature) for msg in messages: role = msg.get("role", "user") @@ -233,7 +397,8 @@ def convert_messages_to_contents( if role == "system": continue - gemini_role = "model" if role == "assistant" else "user" + # 支持 'assistant' 和 'model' 角色(Google history usage) + gemini_role = "model" if role in ("assistant", "model") else "user" raw_content = msg.get("content", "") parts: List[Dict[str, Any]] = [] @@ -307,7 +472,7 @@ def convert_messages_to_contents( fc_part: Dict[str, Any] = { "functionCall": { - "id": original_id, + "id": original_id, # 使用原始ID,不带签名 "name": item.get("name"), "args": item.get("input", {}) or {}, } @@ -321,20 +486,24 @@ def convert_messages_to_contents( elif item_type == "tool_result": output = _extract_tool_result_output(item.get("content")) encoded_tool_use_id = item.get("tool_use_id") or "" + # 解码获取原始ID(functionResponse不需要签名) original_tool_use_id, _ = decode_tool_id_and_signature(encoded_tool_use_id) # 从 tool_result 获取 name,如果没有则从映射中查找 func_name = item.get("name") if not func_name and encoded_tool_use_id: - # 使用编码ID查找,因为映射中存储的是编码ID - func_name = tool_use_names.get(str(encoded_tool_use_id)) + # 使用编码ID查找映射 + tool_info = tool_use_info.get(str(encoded_tool_use_id)) + if tool_info: + func_name = tool_info[0] # 获取 name if not func_name: func_name = "unknown_function" + parts.append( { "functionResponse": { - "id": original_tool_use_id, # 使用解码后的ID以匹配functionCall + "id": original_tool_use_id, # 使用解码后的原始ID以匹配functionCall "name": func_name, "response": {"output": output}, } @@ -472,12 +641,26 @@ async def anthropic_to_gemini_request(payload: Dict[str, Any]) -> Dict[str, Any] messages = payload.get("messages") or [] if not isinstance(messages, list): messages = [] + + # [CRITICAL FIX] 过滤并修复 Thinking 块签名 + # 在转换前先过滤无效的 thinking 块 + filter_invalid_thinking_blocks(messages) # 构建生成配置 generation_config = build_generation_config(payload) # 转换消息内容(始终包含thinking块,由响应端处理) contents = convert_messages_to_contents(messages, include_thinking=True) + + # [CRITICAL FIX] 移除尾部无签名的 thinking 块 + # 对真实请求应用额外的清理 + for content in contents: + role = content.get("role", "") + if role == "model": # 只处理 model/assistant 消息 + parts = content.get("parts", []) + if isinstance(parts, list): + remove_trailing_unsigned_thinking(parts) + contents = reorganize_tool_messages(contents) # 转换工具 @@ -548,10 +731,17 @@ def gemini_to_anthropic_response( # 处理 thinking 块 if part.get("thought") is True: - block: Dict[str, Any] = {"type": "thinking", "thinking": part.get("text", "")} + thinking_text = part.get("text", "") + if thinking_text is None: + thinking_text = "" + + block: Dict[str, Any] = {"type": "thinking", "thinking": str(thinking_text)} + + # 如果有 signature 则添加 signature = part.get("thoughtSignature") if signature: block["signature"] = signature + content.append(block) continue @@ -566,6 +756,8 @@ def gemini_to_anthropic_response( fc = part.get("functionCall", {}) or {} original_id = fc.get("id") or f"toolu_{uuid.uuid4().hex}" signature = part.get("thoughtSignature") + + # 对工具调用ID进行签名编码 encoded_id = encode_tool_id_with_signature(original_id, signature) content.append( { @@ -742,6 +934,10 @@ def _close_block() -> Optional[bytes]: # 处理 thinking 块 if part.get("thought") is True: + thinking_text = part.get("text", "") + signature = part.get("thoughtSignature") + + # 检查是否需要关闭上一个块并开启新的 thinking 块 if current_block_type != "thinking": close_evt = _close_block() if close_evt: @@ -749,7 +945,6 @@ def _close_block() -> Optional[bytes]: current_block_index += 1 current_block_type = "thinking" - signature = part.get("thoughtSignature") current_thinking_signature = signature block: Dict[str, Any] = {"type": "thinking", "thinking": ""} @@ -764,8 +959,30 @@ def _close_block() -> Optional[bytes]: "content_block": block, }, ) + elif signature and signature != current_thinking_signature: + # 签名变化,需要开启新的 thinking 块 + close_evt = _close_block() + if close_evt: + yield close_evt + + current_block_index += 1 + current_block_type = "thinking" + current_thinking_signature = signature + + block_new: Dict[str, Any] = {"type": "thinking", "thinking": ""} + if signature: + block_new["signature"] = signature + + yield _sse_event( + "content_block_start", + { + "type": "content_block_start", + "index": current_block_index, + "content_block": block_new, + }, + ) - thinking_text = part.get("text", "") + # 发送 thinking 文本增量 if thinking_text: yield _sse_event( "content_block_delta", diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index ccb2f2429..be8ea31b0 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -369,34 +369,23 @@ async def normalize_gemini_request( if tools: result["tools"] = clean_tools_for_gemini(tools) - # 4. 清理空的 parts 和未知字段(修复 400 错误:required oneof field 'data' must have one initialized field) - # 同时移除不支持的字段如 cache_control if "contents" in result: - # 定义 part 中允许的字段集合 - ALLOWED_PART_KEYS = { - "text", "inlineData", "fileData", "functionCall", "functionResponse", - "thought", "thoughtSignature" # thinking 相关字段 - } - cleaned_contents = [] for content in result["contents"]: if isinstance(content, dict) and "parts" in content: - # 过滤掉空的或无效的 parts,并移除未知字段 + # 过滤掉空的或无效的 parts valid_parts = [] for part in content["parts"]: if not isinstance(part, dict): continue - # 移除不支持的字段(如 cache_control) - cleaned_part = {k: v for k, v in part.items() if k in ALLOWED_PART_KEYS} - # 检查 part 是否有有效的数据字段 has_valid_data = any( - key in cleaned_part and cleaned_part[key] + key in part and part[key] for key in ["text", "inlineData", "fileData", "functionCall", "functionResponse"] ) if has_valid_data: - valid_parts.append(cleaned_part) + valid_parts.append(part) else: log.warning(f"[GEMINI_FIX] 移除空的或无效的 part: {part}") diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index a033725f5..38c02f7eb 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -159,72 +159,237 @@ def _normalize_function_name(name: str) -> str: return normalized -def _clean_schema_for_gemini(schema: Any) -> Any: +def _resolve_ref(ref: str, root_schema: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ - 清理 JSON Schema,移除 Gemini 不支持的字段 + 解析 $ref 引用 + + Args: + ref: 引用路径,如 "#/definitions/MyType" + root_schema: 根 schema 对象 + + Returns: + 解析后的 schema,如果失败返回 None + """ + if not ref.startswith('#/'): + return None + + path = ref[2:].split('/') + current = root_schema + + for segment in path: + if isinstance(current, dict) and segment in current: + current = current[segment] + else: + return None + + return current if isinstance(current, dict) else None - Gemini API 只支持有限的 OpenAPI 3.0 Schema 属性: - - 支持: type, description, enum, items, properties, required, nullable, format - - 不支持: $schema, $id, $ref, $defs, title, examples, default, readOnly, - exclusiveMaximum, exclusiveMinimum, oneOf, anyOf, allOf, const 等 +def _clean_schema_for_gemini(schema: Any, root_schema: Optional[Dict[str, Any]] = None, visited: Optional[set] = None) -> Any: + """ + 清理 JSON Schema,转换为 Gemini 支持的格式 + + 参考 worker.mjs 的 transformOpenApiSchemaToGemini 实现 + + 处理逻辑: + 1. 解析 $ref 引用 + 2. 合并 allOf 中的 schema + 3. 转换 anyOf 为 enum(如果可能) + 4. 类型映射(string -> STRING) + 5. 处理 ARRAY 的 items(包括 Tuple) + 6. 将 default 值移到 description + 7. 清理不支持的字段 + Args: - schema: JSON Schema 对象(字典、列表或其他值) - + schema: JSON Schema 对象 + root_schema: 根 schema(用于解析 $ref) + visited: 已访问的对象集合(防止循环引用) + Returns: 清理后的 schema """ + # 非字典类型直接返回 if not isinstance(schema, dict): return schema - - # Gemini 不支持的字段 + + # 初始化 + if root_schema is None: + root_schema = schema + if visited is None: + visited = set() + + # 防止循环引用 + schema_id = id(schema) + if schema_id in visited: + return schema + visited.add(schema_id) + + # 创建副本避免修改原对象 + result = {} + + # 1. 处理 $ref + if "$ref" in schema: + resolved = _resolve_ref(schema["$ref"], root_schema) + if resolved: + # 合并解析后的 schema 和当前 schema + import copy + result = copy.deepcopy(resolved) + # 当前 schema 的其他字段会覆盖解析后的字段 + for key, value in schema.items(): + if key != "$ref": + result[key] = value + schema = result + result = {} + + # 2. 处理 allOf(合并所有 schema) + if "allOf" in schema: + all_of_schemas = schema["allOf"] + for item in all_of_schemas: + cleaned_item = _clean_schema_for_gemini(item, root_schema, visited) + + # 合并 properties + if "properties" in cleaned_item: + if "properties" not in result: + result["properties"] = {} + result["properties"].update(cleaned_item["properties"]) + + # 合并 required + if "required" in cleaned_item: + if "required" not in result: + result["required"] = [] + result["required"].extend(cleaned_item["required"]) + + # 合并其他字段(简单覆盖) + for key, value in cleaned_item.items(): + if key not in ["properties", "required"]: + result[key] = value + + # 复制其他字段 + for key, value in schema.items(): + if key not in ["allOf", "properties", "required"]: + result[key] = value + elif key in ["properties", "required"] and key not in result: + result[key] = value + else: + # 复制所有字段 + result = dict(schema) + + # 3. 类型映射(转换为大写) + if "type" in result: + type_value = result["type"] + + # 处理 type: ["string", "null"] 的情况 + if isinstance(type_value, list): + primary_type = next((t for t in type_value if t != "null"), None) + if primary_type: + type_value = primary_type + + # 类型映射 + type_map = { + "string": "STRING", + "number": "NUMBER", + "integer": "INTEGER", + "boolean": "BOOLEAN", + "array": "ARRAY", + "object": "OBJECT", + } + + if isinstance(type_value, str) and type_value.lower() in type_map: + result["type"] = type_map[type_value.lower()] + + # 4. 处理 ARRAY 的 items + if result.get("type") == "ARRAY": + if "items" not in result: + # 没有 items,默认允许任意类型 + result["items"] = {} + elif isinstance(result["items"], list): + # Tuple 定义(items 是数组) + tuple_items = result["items"] + + # 提取类型信息用于 description + tuple_types = [item.get("type", "any") for item in tuple_items] + tuple_desc = f"(Tuple: [{', '.join(tuple_types)}])" + + original_desc = result.get("description", "") + result["description"] = f"{original_desc} {tuple_desc}".strip() + + # 检查是否所有元素类型相同 + first_type = tuple_items[0].get("type") if tuple_items else None + is_homogeneous = all(item.get("type") == first_type for item in tuple_items) + + if is_homogeneous and first_type: + # 同质元组,转换为 List + result["items"] = _clean_schema_for_gemini(tuple_items[0], root_schema, visited) + else: + # 异质元组,Gemini 不支持,设为 {} + result["items"] = {} + else: + # 递归处理 items + result["items"] = _clean_schema_for_gemini(result["items"], root_schema, visited) + + # 5. 处理 anyOf(尝试转换为 enum) + if "anyOf" in result: + any_of_schemas = result["anyOf"] + + # 递归处理每个 schema + cleaned_any_of = [_clean_schema_for_gemini(item, root_schema, visited) for item in any_of_schemas] + + # 尝试提取 enum + if all("const" in item for item in cleaned_any_of): + enum_values = [ + str(item["const"]) + for item in cleaned_any_of + if item.get("const") not in ["", None] + ] + if enum_values: + result["type"] = "STRING" + result["enum"] = enum_values + elif "type" not in result: + # 如果不是 enum,尝试取第一个有效的类型定义 + first_valid = next((item for item in cleaned_any_of if item.get("type") or item.get("enum")), None) + if first_valid: + result.update(first_valid) + + # 删除 anyOf + del result["anyOf"] + + # 6. 将 default 值移到 description + if "default" in result: + default_value = result["default"] + original_desc = result.get("description", "") + result["description"] = f"{original_desc} (Default: {json.dumps(default_value)})".strip() + del result["default"] + + # 7. 清理不支持的字段 unsupported_keys = { - "$schema", - "$id", - "$ref", - "$defs", - "definitions", - "example", - "examples", - "readOnly", - "writeOnly", - "default", - "exclusiveMaximum", - "exclusiveMinimum", - "oneOf", - "anyOf", - "allOf", - "const", - "additionalItems", - "contains", - "patternProperties", - "dependencies", - "propertyNames", - "if", - "then", - "else", - "contentEncoding", - "contentMediaType", + "title", "$schema", "$ref", "strict", "exclusiveMaximum", + "exclusiveMinimum", "additionalProperties", "oneOf", "allOf", + "$defs", "definitions", "example", "examples", "readOnly", + "writeOnly", "const", "additionalItems", "contains", + "patternProperties", "dependencies", "propertyNames", + "if", "then", "else", "contentEncoding", "contentMediaType" } - - cleaned = {} - for key, value in schema.items(): + + for key in list(result.keys()): if key in unsupported_keys: - continue - if isinstance(value, dict): - cleaned[key] = _clean_schema_for_gemini(value) - elif isinstance(value, list): - cleaned[key] = [ - _clean_schema_for_gemini(item) if isinstance(item, dict) else item for item in value - ] - else: - cleaned[key] = value - - # 确保有 type 字段(如果有 properties 但没有 type) - if "properties" in cleaned and "type" not in cleaned: - cleaned["type"] = "object" - - return cleaned + del result[key] + + # 8. 递归处理 properties + if "properties" in result: + cleaned_props = {} + for prop_name, prop_schema in result["properties"].items(): + cleaned_props[prop_name] = _clean_schema_for_gemini(prop_schema, root_schema, visited) + result["properties"] = cleaned_props + + # 9. 确保有 type 字段(如果有 properties 但没有 type) + if "properties" in result and "type" not in result: + result["type"] = "OBJECT" + + # 10. 去重 required 数组 + if "required" in result and isinstance(result["required"], list): + result["required"] = list(dict.fromkeys(result["required"])) # 保持顺序去重 + + return result def fix_tool_call_args_types( @@ -445,6 +610,76 @@ def convert_tool_message_to_function_response(message, all_messages: List = None return {"functionResponse": {"id": original_tool_call_id, "name": name, "response": response_data}} +def _reverse_transform_value(value: Any) -> Any: + """ + 将值转换回原始类型(Gemini 可能将所有值转为字符串) + + 参考 worker.mjs 的 reverseTransformValue + + Args: + value: 要转换的值 + + Returns: + 转换后的值 + """ + if not isinstance(value, str): + return value + + # 布尔值 + if value == 'true': + return True + if value == 'false': + return False + + # null + if value == 'null': + return None + + # 数字(确保字符串确实是纯数字) + if value.strip() and not value.startswith('0') and value.replace('.', '', 1).replace('-', '', 1).replace('+', '', 1).isdigit(): + try: + # 尝试转换为数字 + num_value = float(value) + # 如果是整数,返回 int + if num_value == int(num_value): + return int(num_value) + return num_value + except ValueError: + pass + + # 其他情况保持字符串 + return value + + +def _reverse_transform_args(args: Any) -> Any: + """ + 递归转换函数参数,将字符串转回原始类型 + + 参考 worker.mjs 的 reverseTransformArgs + + Args: + args: 函数参数(可能是字典、列表或其他类型) + + Returns: + 转换后的参数 + """ + if not isinstance(args, (dict, list)): + return args + + if isinstance(args, list): + return [_reverse_transform_args(item) for item in args] + + # 处理字典 + result = {} + for key, value in args.items(): + if isinstance(value, (dict, list)): + result[key] = _reverse_transform_args(value) + else: + result[key] = _reverse_transform_value(value) + + return result + + def extract_tool_calls_from_parts( parts: List[Dict[str, Any]], is_streaming: bool = False ) -> Tuple[List[Dict[str, Any]], str]: @@ -471,12 +706,17 @@ def extract_tool_calls_from_parts( signature = part.get("thoughtSignature") encoded_id = encode_tool_id_with_signature(original_id, signature) + # 获取参数并转换类型 + args = function_call.get("args", {}) + # 将字符串类型的值转回原始类型 + args = _reverse_transform_args(args) + tool_call = { "id": encoded_id, "type": "function", "function": { "name": function_call.get("name", "nameless_function"), - "arguments": json.dumps(function_call.get("args", {})), + "arguments": json.dumps(args), }, } # 流式响应需要 index 字段 @@ -693,12 +933,19 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di # 构建生成配置 generation_config = {} + model = openai_request.get("model", "") + + # 基础参数映射 if "temperature" in openai_request: generation_config["temperature"] = openai_request["temperature"] if "top_p" in openai_request: generation_config["topP"] = openai_request["top_p"] - if "max_tokens" in openai_request: - generation_config["maxOutputTokens"] = openai_request["max_tokens"] + if "top_k" in openai_request: + generation_config["topK"] = openai_request["top_k"] + if "max_tokens" in openai_request or "max_completion_tokens" in openai_request: + # max_completion_tokens 优先于 max_tokens + max_tokens = openai_request.get("max_completion_tokens") or openai_request.get("max_tokens") + generation_config["maxOutputTokens"] = max_tokens if "stop" in openai_request: stop = openai_request["stop"] generation_config["stopSequences"] = [stop] if isinstance(stop, str) else stop @@ -710,10 +957,26 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di generation_config["candidateCount"] = openai_request["n"] if "seed" in openai_request: generation_config["seed"] = openai_request["seed"] + + # 处理 response_format if "response_format" in openai_request and openai_request["response_format"]: - if openai_request["response_format"].get("type") == "json_object": + response_format = openai_request["response_format"] + format_type = response_format.get("type") + + if format_type == "json_schema": + # JSON Schema 模式 + if "json_schema" in response_format and "schema" in response_format["json_schema"]: + schema = response_format["json_schema"]["schema"] + # 清理 schema + generation_config["responseSchema"] = _clean_schema_for_gemini(schema) + generation_config["responseMimeType"] = "application/json" + elif format_type == "json_object": + # JSON Object 模式 generation_config["responseMimeType"] = "application/json" - + elif format_type == "text": + # Text 模式 + generation_config["responseMimeType"] = "text/plain" + # 如果contents为空,添加默认用户消息 if not contents: contents.append({"role": "user", "parts": [{"text": "请根据系统指令回答。"}]}) @@ -812,25 +1075,57 @@ def convert_gemini_to_openai_response( # 提取工具调用和文本内容 tool_calls, text_content = extract_tool_calls_from_parts(parts) - # 提取图片数据 - images = [] + # 提取多种类型的内容 + content_parts = [] + reasoning_parts = [] + for part in parts: - if "inlineData" in part: + # 处理 executableCode(代码生成) + if "executableCode" in part: + exec_code = part["executableCode"] + lang = exec_code.get("language", "python").lower() + code = exec_code.get("code", "") + # 添加代码块(前后加换行符确保 Markdown 渲染正确) + content_parts.append(f"\n```{lang}\n{code}\n```\n") + + # 处理 codeExecutionResult(代码执行结果) + elif "codeExecutionResult" in part: + result = part["codeExecutionResult"] + outcome = result.get("outcome") + output = result.get("output", "") + + if output: + label = "output" if outcome == "OUTCOME_OK" else "error" + content_parts.append(f"\n```{label}\n{output}\n```\n") + + # 处理 thought(思考内容) + elif part.get("thought", False) and "text" in part: + reasoning_parts.append(part["text"]) + + # 处理普通文本(非思考内容) + elif "text" in part and not part.get("thought", False): + # 这部分已经在 extract_tool_calls_from_parts 中处理 + pass + + # 处理 inlineData(图片) + elif "inlineData" in part: inline_data = part["inlineData"] mime_type = inline_data.get("mimeType", "image/png") base64_data = inline_data.get("data", "") - images.append({ - "type": "image_url", - "image_url": { - "url": f"data:{mime_type};base64,{base64_data}" - } - }) - - # 提取 reasoning content - reasoning_content = "" - for part in parts: - if part.get("thought", False) and "text" in part: - reasoning_content += part["text"] + # 使用 Markdown 格式 + content_parts.append(f"![gemini-generated-content](data:{mime_type};base64,{base64_data})") + + # 合并所有内容部分 + if content_parts: + # 使用双换行符连接各部分,确保块之间有间距 + additional_content = "\n\n".join(content_parts) + if text_content: + text_content = text_content + "\n\n" + additional_content + else: + text_content = additional_content + + # 合并 reasoning content + reasoning_content = "\n\n".join(reasoning_parts) if reasoning_parts else "" # 构建消息对象 message = {"role": role} @@ -840,14 +1135,6 @@ def convert_gemini_to_openai_response( message["tool_calls"] = tool_calls message["content"] = text_content if text_content else None finish_reason = "tool_calls" - # 如果有图片 - elif images: - content_list = [] - if text_content: - content_list.append({"type": "text", "text": text_content}) - content_list.extend(images) - message["content"] = content_list - finish_reason = _map_finish_reason(candidate.get("finishReason")) else: message["content"] = text_content finish_reason = _map_finish_reason(candidate.get("finishReason")) @@ -951,25 +1238,54 @@ def convert_gemini_to_openai_stream( # 提取工具调用和文本内容 (流式需要 index) tool_calls, text_content = extract_tool_calls_from_parts(parts, is_streaming=True) - # 提取图片数据 - images = [] + # 提取多种类型的内容 + content_parts = [] + reasoning_parts = [] + for part in parts: - if "inlineData" in part: + # 处理 executableCode(代码生成) + if "executableCode" in part: + exec_code = part["executableCode"] + lang = exec_code.get("language", "python").lower() + code = exec_code.get("code", "") + content_parts.append(f"\n```{lang}\n{code}\n```\n") + + # 处理 codeExecutionResult(代码执行结果) + elif "codeExecutionResult" in part: + result = part["codeExecutionResult"] + outcome = result.get("outcome") + output = result.get("output", "") + + if output: + label = "output" if outcome == "OUTCOME_OK" else "error" + content_parts.append(f"\n```{label}\n{output}\n```\n") + + # 处理 thought(思考内容) + elif part.get("thought", False) and "text" in part: + reasoning_parts.append(part["text"]) + + # 处理普通文本(非思考内容) + elif "text" in part and not part.get("thought", False): + # 这部分已经在 extract_tool_calls_from_parts 中处理 + pass + + # 处理 inlineData(图片) + elif "inlineData" in part: inline_data = part["inlineData"] mime_type = inline_data.get("mimeType", "image/png") base64_data = inline_data.get("data", "") - images.append({ - "type": "image_url", - "image_url": { - "url": f"data:{mime_type};base64,{base64_data}" - } - }) - - # 提取 reasoning content - reasoning_content = "" - for part in parts: - if part.get("thought", False) and "text" in part: - reasoning_content += part["text"] + content_parts.append(f"![gemini-generated-content](data:{mime_type};base64,{base64_data})") + + # 合并所有内容部分 + if content_parts: + additional_content = "\n\n".join(content_parts) + if text_content: + text_content = text_content + "\n\n" + additional_content + else: + text_content = additional_content + + # 合并 reasoning content + reasoning_content = "\n\n".join(reasoning_parts) if reasoning_parts else "" # 构建 delta 对象 delta = {} @@ -978,13 +1294,6 @@ def convert_gemini_to_openai_stream( delta["tool_calls"] = tool_calls if text_content: delta["content"] = text_content - elif images: - # 流式响应中的图片: 以 markdown 格式返回 - markdown_images = [f"![Generated Image]({img['image_url']['url']})" for img in images] - if text_content: - delta["content"] = text_content + "\n\n" + "\n\n".join(markdown_images) - else: - delta["content"] = "\n\n".join(markdown_images) elif text_content: delta["content"] = text_content From b263b982ed7a6c39e40ba7a316cb01a0227437f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 08:56:37 +0000 Subject: [PATCH 046/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 952521cea..3b65f0eb2 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=7e8afa2a0d68405134abe3e90b5a756b2de51bf4 -short_hash=7e8afa2 -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 11:55:08 +0800 +full_hash=ea6b6f4f2045db2ce7784693a77f1e3e89d3f8bc +short_hash=ea6b6f4 +message=修正格式转换 +date=2026-01-11 16:56:27 +0800 From bd3ff2ae376f4810c9ab7803d5079559f063b2da Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 17:26:57 +0800 Subject: [PATCH 047/211] Update utils.py --- src/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.py b/src/utils.py index 83ca21dd9..5d3d2d6f8 100644 --- a/src/utils.py +++ b/src/utils.py @@ -222,7 +222,7 @@ async def endpoint(token: str = Depends(authenticate_flexible)): # 验证 token if token != password: - log.error(f"Authentication failed using {auth_method}") + log.debug(f"Authentication failed using {auth_method}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="密码错误" From b4237ac2f10ca1e13e0434a5e626048e5d6804c6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 09:27:20 +0000 Subject: [PATCH 048/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 3b65f0eb2..e1b1fce92 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=ea6b6f4f2045db2ce7784693a77f1e3e89d3f8bc -short_hash=ea6b6f4 -message=修正格式转换 -date=2026-01-11 16:56:27 +0800 +full_hash=d0ba30f327f4a2a473b6dc765e75211c35a8d25a +short_hash=d0ba30f +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-11 17:27:07 +0800 From 51041fdd96814cd448f8856569c2f50806733d52 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 18:06:39 +0800 Subject: [PATCH 049/211] =?UTF-8?q?claude=E8=B7=AF=E7=94=B1=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0token=E8=AE=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/antigravity/anthropic.py | 78 ++++++++++++++++++++++++++++- src/router/base_router.py | 34 +------------ src/router/geminicli/anthropic.py | 78 ++++++++++++++++++++++++++++- 3 files changed, 153 insertions(+), 37 deletions(-) diff --git a/src/router/antigravity/anthropic.py b/src/router/antigravity/anthropic.py index 308986455..50e74ac20 100644 --- a/src/router/antigravity/anthropic.py +++ b/src/router/antigravity/anthropic.py @@ -16,11 +16,11 @@ import json # 第三方库 -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import JSONResponse, StreamingResponse # 本地模块 - 配置和日志 -from config import get_anti_truncation_max_attempts +from config import get_anti_truncation_max_attempts, get_api_password from log import log # 本地模块 - 工具和认证 @@ -47,6 +47,9 @@ # 本地模块 - 任务管理 from src.task_manager import create_managed_task +# 本地模块 - Token估算 +from src.token_estimator import estimate_input_tokens + # ==================== 路由器初始化 ==================== @@ -364,6 +367,77 @@ async def gemini_chunk_wrapper(): return StreamingResponse(normal_stream_generator(), media_type="text/event-stream") +@router.post("/antigravity/v1/messages/count_tokens") +async def count_tokens( + request: Request, + _token: str = Depends(authenticate_bearer) +): + """ + 处理Anthropic格式的token计数请求 + + Args: + request: FastAPI请求对象 + _token: Bearer认证令牌(由Depends验证) + + Returns: + JSONResponse: 包含input_tokens的响应 + """ + try: + payload = await request.json() + except Exception as e: + return JSONResponse( + status_code=400, + content={"type": "error", "error": {"type": "invalid_request_error", "message": f"JSON 解析失败: {str(e)}"}} + ) + + if not isinstance(payload, dict): + return JSONResponse( + status_code=400, + content={"type": "error", "error": {"type": "invalid_request_error", "message": "请求体必须为 JSON object"}} + ) + + if not payload.get("model") or not isinstance(payload.get("messages"), list): + return JSONResponse( + status_code=400, + content={"type": "error", "error": {"type": "invalid_request_error", "message": "缺少必填字段:model / messages"}} + ) + + try: + client_host = request.client.host if request.client else "unknown" + client_port = request.client.port if request.client else "unknown" + except Exception: + client_host = "unknown" + client_port = "unknown" + + thinking_present = "thinking" in payload + thinking_value = payload.get("thinking") + thinking_summary = None + if thinking_present: + if isinstance(thinking_value, dict): + thinking_summary = { + "type": thinking_value.get("type"), + "budget_tokens": thinking_value.get("budget_tokens"), + } + else: + thinking_summary = thinking_value + + user_agent = request.headers.get("user-agent", "") + log.info( + f"[ANTIGRAVITY-ANTHROPIC] /messages/count_tokens 收到请求: client={client_host}:{client_port}, " + f"model={payload.get('model')}, messages={len(payload.get('messages') or [])}, " + f"thinking_present={thinking_present}, thinking={thinking_summary}, ua={user_agent}" + ) + + # 简单估算 + input_tokens = 0 + try: + input_tokens = estimate_input_tokens(payload) + except Exception as e: + log.error(f"[ANTIGRAVITY-ANTHROPIC] token 估算失败: {e}") + + return JSONResponse(content={"input_tokens": input_tokens}) + + # ==================== 测试代码 ==================== if __name__ == "__main__": diff --git a/src/router/base_router.py b/src/router/base_router.py index e17b64818..f9d2b37d7 100644 --- a/src/router/base_router.py +++ b/src/router/base_router.py @@ -3,42 +3,10 @@ 提供模型列表处理、通用响应等共同功能 """ -from typing import List, Optional +from typing import List from src.models import Model, ModelList - -# ==================== 模型列表处理 ==================== - -def expand_models_with_features( - base_models: List[str], - features: Optional[List[str]] = None -) -> List[str]: - """ - 使用特性前缀扩展模型列表 - - Args: - base_models: 基础模型列表 - features: 特性前缀列表,如 ["流式抗截断", "假流式"] - - Returns: - 扩展后的模型列表(包含原始模型和特性变体) - """ - if not features: - return base_models.copy() - - expanded = [] - for model in base_models: - # 添加原始模型 - expanded.append(model) - - # 添加特性变体 - for feature in features: - expanded.append(f"{feature}/{model}") - - return expanded - - def create_openai_model_list( model_ids: List[str], owned_by: str = "google" diff --git a/src/router/geminicli/anthropic.py b/src/router/geminicli/anthropic.py index c99958b57..14dd5c4b8 100644 --- a/src/router/geminicli/anthropic.py +++ b/src/router/geminicli/anthropic.py @@ -16,11 +16,11 @@ import json # 第三方库 -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import JSONResponse, StreamingResponse # 本地模块 - 配置和日志 -from config import get_anti_truncation_max_attempts +from config import get_anti_truncation_max_attempts, get_api_password from log import log # 本地模块 - 工具和认证 @@ -47,6 +47,9 @@ # 本地模块 - 任务管理 from src.task_manager import create_managed_task +# 本地模块 - Token估算 +from src.token_estimator import estimate_input_tokens + # ==================== 路由器初始化 ==================== @@ -364,6 +367,77 @@ async def gemini_chunk_wrapper(): return StreamingResponse(normal_stream_generator(), media_type="text/event-stream") +@router.post("/v1/messages/count_tokens") +async def count_tokens( + request: Request, + _token: str = Depends(authenticate_bearer) +): + """ + 处理Anthropic格式的token计数请求 + + Args: + request: FastAPI请求对象 + _token: Bearer认证令牌(由Depends验证) + + Returns: + JSONResponse: 包含input_tokens的响应 + """ + try: + payload = await request.json() + except Exception as e: + return JSONResponse( + status_code=400, + content={"type": "error", "error": {"type": "invalid_request_error", "message": f"JSON 解析失败: {str(e)}"}} + ) + + if not isinstance(payload, dict): + return JSONResponse( + status_code=400, + content={"type": "error", "error": {"type": "invalid_request_error", "message": "请求体必须为 JSON object"}} + ) + + if not payload.get("model") or not isinstance(payload.get("messages"), list): + return JSONResponse( + status_code=400, + content={"type": "error", "error": {"type": "invalid_request_error", "message": "缺少必填字段:model / messages"}} + ) + + try: + client_host = request.client.host if request.client else "unknown" + client_port = request.client.port if request.client else "unknown" + except Exception: + client_host = "unknown" + client_port = "unknown" + + thinking_present = "thinking" in payload + thinking_value = payload.get("thinking") + thinking_summary = None + if thinking_present: + if isinstance(thinking_value, dict): + thinking_summary = { + "type": thinking_value.get("type"), + "budget_tokens": thinking_value.get("budget_tokens"), + } + else: + thinking_summary = thinking_value + + user_agent = request.headers.get("user-agent", "") + log.info( + f"[GEMINICLI-ANTHROPIC] /messages/count_tokens 收到请求: client={client_host}:{client_port}, " + f"model={payload.get('model')}, messages={len(payload.get('messages') or [])}, " + f"thinking_present={thinking_present}, thinking={thinking_summary}, ua={user_agent}" + ) + + # 简单估算 + input_tokens = 0 + try: + input_tokens = estimate_input_tokens(payload) + except Exception as e: + log.error(f"[GEMINICLI-ANTHROPIC] token 估算失败: {e}") + + return JSONResponse(content={"input_tokens": input_tokens}) + + # ==================== 测试代码 ==================== if __name__ == "__main__": From f4e03dca95fc9b8e91b1097335f042840b1633ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 10:06:51 +0000 Subject: [PATCH 050/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index e1b1fce92..af981ae05 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=d0ba30f327f4a2a473b6dc765e75211c35a8d25a -short_hash=d0ba30f -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 17:27:07 +0800 +full_hash=51041fdd96814cd448f8856569c2f50806733d52 +short_hash=51041fd +message=claude路由增加token计数 +date=2026-01-11 18:06:39 +0800 From cfb439ea239d0a9eb37f2d6c02da8c5546a884d8 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 18:42:24 +0800 Subject: [PATCH 051/211] =?UTF-8?q?=E9=9D=9E=E6=B5=81=E5=BC=8F=E8=A7=A3?= =?UTF-8?q?=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/antigravity/gemini.py | 16 ++++++++++++++-- src/router/geminicli/gemini.py | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/router/antigravity/gemini.py b/src/router/antigravity/gemini.py index 4640f4dc3..8b5713f1f 100644 --- a/src/router/antigravity/gemini.py +++ b/src/router/antigravity/gemini.py @@ -105,9 +105,21 @@ async def generate_content( from src.api.antigravity import non_stream_request response = await non_stream_request(body=api_request) - # 直接返回响应(response已经是FastAPI Response对象) + # 解包装响应:Antigravity API 可能返回的格式有额外的 response 包装层 + # 需要提取并返回标准 Gemini 格式 # 保持 Gemini 原生的 inlineData 格式,不进行 Markdown 转换 - return response + try: + if response.status_code == 200: + response_data = json.loads(response.body if hasattr(response, 'body') else response.content) + # 如果有 response 包装,解包装它 + if "response" in response_data: + unwrapped_data = response_data["response"] + return JSONResponse(content=unwrapped_data) + # 错误响应或没有 response 字段,直接返回 + return response + except Exception as e: + log.warning(f"Failed to unwrap response: {e}, returning original response") + return response @router.post("/antigravity/v1beta/models/{model:path}:streamGenerateContent") @router.post("/antigravity/v1/models/{model:path}:streamGenerateContent") diff --git a/src/router/geminicli/gemini.py b/src/router/geminicli/gemini.py index f1d889101..0b2ef9238 100644 --- a/src/router/geminicli/gemini.py +++ b/src/router/geminicli/gemini.py @@ -105,8 +105,20 @@ async def generate_content( from src.api.geminicli import non_stream_request response = await non_stream_request(body=api_request) - # 直接返回响应(response已经是FastAPI Response对象) - return response + # 解包装响应:GeminiCli API 返回的格式有额外的 response 包装层 + # 需要提取 response.response 并返回标准 Gemini 格式 + try: + if response.status_code == 200: + response_data = json.loads(response.body if hasattr(response, 'body') else response.content) + # 如果有 response 包装,解包装它 + if "response" in response_data: + unwrapped_data = response_data["response"] + return JSONResponse(content=unwrapped_data) + # 错误响应或没有 response 字段,直接返回 + return response + except Exception as e: + log.warning(f"Failed to unwrap response: {e}, returning original response") + return response @router.post("/v1beta/models/{model:path}:streamGenerateContent") @router.post("/v1/models/{model:path}:streamGenerateContent") From 1bc2c41185a8c2b78d9e5789034ad5753bdb1af5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 10:42:35 +0000 Subject: [PATCH 052/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index af981ae05..ea1d7d194 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=51041fdd96814cd448f8856569c2f50806733d52 -short_hash=51041fd -message=claude路由增加token计数 -date=2026-01-11 18:06:39 +0800 +full_hash=cfb439ea239d0a9eb37f2d6c02da8c5546a884d8 +short_hash=cfb439e +message=非流式解包 +date=2026-01-11 18:42:24 +0800 From 437660b55a877ecd93d2faeb9d16acd6c41f7598 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 19:09:33 +0800 Subject: [PATCH 053/211] =?UTF-8?q?=E5=A2=9E=E5=8A=A0tool=5Fchoice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 51 ++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index d4004b347..d7f630b12 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -569,7 +569,48 @@ def reorganize_tool_messages(contents: List[Dict[str, Any]]) -> List[Dict[str, A # ============================================================================ -# 7. Generation Config 构建 +# 7. Tool Choice 转换 +# ============================================================================ + +def convert_tool_choice_to_tool_config(tool_choice: Any) -> Optional[Dict[str, Any]]: + """ + 将 Anthropic tool_choice 转换为 Gemini toolConfig + + Args: + tool_choice: Anthropic 格式的 tool_choice + - {"type": "auto"}: 模型自动决定是否使用工具 + - {"type": "any"}: 模型必须使用工具 + - {"type": "tool", "name": "tool_name"}: 模型必须使用指定工具 + + Returns: + Gemini 格式的 toolConfig,如果无效则返回 None + """ + if not tool_choice: + return None + + if isinstance(tool_choice, dict): + choice_type = tool_choice.get("type") + + if choice_type == "auto": + return {"functionCallingConfig": {"mode": "AUTO"}} + elif choice_type == "any": + return {"functionCallingConfig": {"mode": "ANY"}} + elif choice_type == "tool": + tool_name = tool_choice.get("name") + if tool_name: + return { + "functionCallingConfig": { + "mode": "ANY", + "allowedFunctionNames": [tool_name], + } + } + + # 无效或不支持的 tool_choice,返回 None + return None + + +# ============================================================================ +# 8. Generation Config 构建 # ============================================================================ def build_generation_config(payload: Dict[str, Any]) -> Dict[str, Any]: @@ -633,6 +674,7 @@ async def anthropic_to_gemini_request(payload: Dict[str, Any]) -> Dict[str, Any] - generationConfig: 生成配置 - systemInstruction: 系统指令 (如果有) - tools: 工具定义 (如果有) + - toolConfig: 工具调用配置 (如果有 tool_choice) """ # 处理连续的system消息(兼容性模式) payload = await merge_system_messages(payload) @@ -665,6 +707,9 @@ async def anthropic_to_gemini_request(payload: Dict[str, Any]) -> Dict[str, Any] # 转换工具 tools = convert_tools(payload.get("tools")) + + # 转换 tool_choice + tool_config = convert_tool_choice_to_tool_config(payload.get("tool_choice")) # 构建基础请求数据 gemini_request = { @@ -678,6 +723,10 @@ async def anthropic_to_gemini_request(payload: Dict[str, Any]) -> Dict[str, Any] if tools: gemini_request["tools"] = tools + + # 添加 toolConfig(如果有 tool_choice) + if tool_config: + gemini_request["toolConfig"] = tool_config return gemini_request From f23ac103a7626e0eff5def26d9ff54655dd5e6ca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 11:10:17 +0000 Subject: [PATCH 054/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index ea1d7d194..f22326e31 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=cfb439ea239d0a9eb37f2d6c02da8c5546a884d8 -short_hash=cfb439e -message=非流式解包 -date=2026-01-11 18:42:24 +0800 +full_hash=26b050662577200323abcf2b961de9673b1d8fe6 +short_hash=26b0506 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-11 19:10:04 +0800 From d41cad935b056d5b4ebf1fbbc8c1bfd9d35fd4cb Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 19:41:30 +0800 Subject: [PATCH 055/211] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 18 ++-- src/api/geminicli.py | 181 ++++++++++++++++++++++++++++------------- 2 files changed, 132 insertions(+), 67 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 096094244..dee316240 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -182,8 +182,8 @@ async def refresh_credential(): status_code = chunk.status_code last_error_response = chunk # 记录最后一次错误 - # 如果错误码是429或者不在禁用码当中,做好记录后进行重试 - if status_code == 429 or status_code not in DISABLE_ERROR_CODES: + # 如果错误码是429或者在禁用码当中,做好记录后进行重试 + if status_code == 429 or status_code in DISABLE_ERROR_CODES: # 解析错误响应内容 try: error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) @@ -222,12 +222,12 @@ async def refresh_credential(): yield chunk return else: - # 错误码在禁用码当中,直接返回,无需重试 + # 错误码不在禁用码当中,直接返回,无需重试 try: error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) - log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") + log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") except Exception: - log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") + log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="antigravity", model_key=model_name @@ -450,7 +450,7 @@ async def refresh_credential(): ) # 判断是否需要重试 - if status_code == 429 or status_code not in DISABLE_ERROR_CODES: + if status_code == 429 or status_code in DISABLE_ERROR_CODES: try: error_text = response.text log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") @@ -486,12 +486,12 @@ async def refresh_credential(): log.error(f"[ANTIGRAVITY] 达到最大重试次数或不应重试,返回原始错误") return last_error_response else: - # 错误码在禁用码当中,直接返回,无需重试 + # 错误码不在禁用码当中,直接返回,无需重试 try: error_text = response.text - log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") + log.error(f"[ANTIGRAVITY] 非流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") except Exception: - log.error(f"[ANTIGRAVITY] 非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") + log.error(f"[ANTIGRAVITY] 非流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="antigravity", model_key=model_name diff --git a/src/api/geminicli.py b/src/api/geminicli.py index 21dcce3af..fa40b23b8 100644 --- a/src/api/geminicli.py +++ b/src/api/geminicli.py @@ -165,9 +165,30 @@ async def stream_request( DISABLE_ERROR_CODES = await get_auto_ban_error_codes() # 禁用凭证的错误码 last_error_response = None # 记录最后一次的错误响应 + + # 内部函数:获取新凭证并更新headers + async def refresh_credential(): + nonlocal current_file, credential_data, auth_headers, final_payload, target_url + cred_result = await credential_manager.get_valid_credential( + mode="geminicli", model_key=model_group + ) + if not cred_result: + return None + current_file, credential_data = cred_result + try: + auth_headers, final_payload, target_url = await prepare_request_headers_and_payload( + body, credential_data, + f"{await get_code_assist_endpoint()}/v1internal:streamGenerateContent?alt=sse" + ) + if headers: + auth_headers.update(headers) + return True + except Exception: + return None for attempt in range(max_retries + 1): success_recorded = False # 标记是否已记录成功 + need_retry = False # 标记是否需要重试 try: async for chunk in stream_post_async( @@ -181,14 +202,14 @@ async def stream_request( status_code = chunk.status_code last_error_response = chunk # 记录最后一次错误 - # 如果错误码是429或者不在禁用码当中,做好记录后进行重试 - if status_code == 429 or status_code not in DISABLE_ERROR_CODES: + # 如果错误码是429或者在禁用码当中,做好记录后进行重试 + if status_code == 429 or status_code in DISABLE_ERROR_CODES: # 解析错误响应内容 try: error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) - log.warning(f"流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") + log.warning(f"[GEMINICLI STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") except Exception: - log.warning(f"流式请求失败 (status={status_code}), 凭证: {current_file}") + log.warning(f"[GEMINICLI STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}") # 记录错误 cooldown_until = None @@ -213,43 +234,20 @@ async def stream_request( ) if should_retry and attempt < max_retries: - # 重新获取凭证并重试 - log.info(f"[STREAM] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") - await asyncio.sleep(retry_interval) - - # 获取新凭证 - cred_result = await credential_manager.get_valid_credential( - mode="geminicli", model_key=model_group - ) - if not cred_result: - log.error("[STREAM] 重试时无可用凭证") - yield Response( - content=json.dumps({"error": "当前无可用凭证"}), - status_code=500, - media_type="application/json" - ) - return - - current_file, credential_data = cred_result - auth_headers, final_payload, target_url = await prepare_request_headers_and_payload( - body, credential_data, - f"{await get_code_assist_endpoint()}/v1internal:streamGenerateContent?alt=sse" - ) - if headers: - auth_headers.update(headers) - break # 跳出内层循环,重新请求 + need_retry = True + break # 跳出内层循环,准备重试 else: # 不重试,直接返回原始错误 - log.error(f"[STREAM] 达到最大重试次数或不应重试,返回原始错误") + log.error(f"[GEMINICLI STREAM] 达到最大重试次数或不应重试,返回原始错误") yield chunk return else: - # 错误码在禁用码当中,直接返回,无需重试 + # 错误码不在禁用码当中,直接返回,无需重试 try: error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) - log.error(f"流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") + log.error(f"[GEMINICLI STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") except Exception: - log.error(f"流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") + log.error(f"[GEMINICLI STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="geminicli", model_key=model_group @@ -264,21 +262,57 @@ async def stream_request( credential_manager, current_file, mode="geminicli", model_key=model_group ) success_recorded = True + log.info(f"[GEMINICLI STREAM] 开始接收流式响应,模型: {model_name}") yield chunk - # 流式请求成功完成,退出重试循环 - return + # 流式请求完成,检查结果 + if success_recorded: + log.info(f"[GEMINICLI STREAM] 流式响应完成,模型: {model_name}") + return + elif not need_retry: + # 没有收到任何数据(空回复),需要重试 + log.warning(f"[GEMINICLI STREAM] 收到空回复,无任何内容,凭证: {current_file}") + await record_api_call_error( + credential_manager, current_file, 200, + None, mode="geminicli", model_key=model_group + ) + + if attempt < max_retries: + need_retry = True + else: + log.error(f"[GEMINICLI STREAM] 空回复达到最大重试次数") + yield Response( + content=json.dumps({"error": "服务返回空回复"}), + status_code=500, + media_type="application/json" + ) + return + + # 统一处理重试 + if need_retry: + log.info(f"[GEMINICLI STREAM] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") + await asyncio.sleep(retry_interval) + + if not await refresh_credential(): + log.error("[GEMINICLI STREAM] 重试时无可用凭证或刷新失败") + yield Response( + content=json.dumps({"error": "当前无可用凭证"}), + status_code=500, + media_type="application/json" + ) + return + continue # 重试 except Exception as e: - log.error(f"流式请求异常: {e}, 凭证: {current_file}") + log.error(f"[GEMINICLI STREAM] 流式请求异常: {e}, 凭证: {current_file}") if attempt < max_retries: - log.info(f"[STREAM] 异常后重试 (attempt {attempt + 2}/{max_retries + 1})...") + log.info(f"[GEMINICLI STREAM] 异常后重试 (attempt {attempt + 2}/{max_retries + 1})...") await asyncio.sleep(retry_interval) continue else: - # 所有重试都失败,返回最后一次的错误(如果有)或500错误 - log.error(f"[STREAM] 所有重试均失败,最后异常: {e}") + # 所有重试都失败,返回最后一次的错误(如果有) + log.error(f"[GEMINICLI STREAM] 所有重试均失败,最后异常: {e}") yield last_error_response @@ -385,19 +419,62 @@ async def non_stream_request( ) # 判断是否需要重试 - if status_code == 429 or status_code not in DISABLE_ERROR_CODES: - try: - error_text = response.text - log.warning(f"非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") - except Exception: - log.warning(f"非流式请求失败 (status={status_code}), 凭证: {current_file}") + # 获取错误文本 + try: + error_text = response.text + error_msg = f"非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}" + except Exception: + error_text = "" + error_msg = f"非流式请求失败 (status={status_code}), 凭证: {current_file}" + + # 如果错误码在禁用码当中,禁用该凭证 + if status_code in DISABLE_ERROR_CODES: + log.error(f"非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500] if error_text else '无'}") + # 记录错误并禁用凭证 + await record_api_call_error( + credential_manager, current_file, status_code, + None, mode="geminicli", model_key=model_group + ) + # 尝试切换到新凭证并重试 + if attempt < max_retries: + log.info(f"[NON-STREAM] 切换凭证并重试 (attempt {attempt + 2}/{max_retries + 1})...") + await asyncio.sleep(retry_interval) + + # 获取新凭证 + cred_result = await credential_manager.get_valid_credential( + mode="geminicli", model_key=model_group + ) + if not cred_result: + log.error("[NON-STREAM] 重试时无可用凭证") + return Response( + content=json.dumps({"error": "当前无可用凭证"}), + status_code=500, + media_type="application/json" + ) + + current_file, credential_data = cred_result + auth_headers, final_payload, target_url = await prepare_request_headers_and_payload( + body, credential_data, + f"{await get_code_assist_endpoint()}/v1internal:generateContent" + ) + if headers: + auth_headers.update(headers) + continue # 重试 + else: + # 达到最大重试次数 + log.error(f"[NON-STREAM] 达到最大重试次数,返回原始错误") + return last_error_response + else: + # 错误码不在禁用码当中(如429等),做好记录后进行重试 + log.warning(error_msg) # 记录错误 cooldown_until = None if status_code == 429: # 尝试解析冷却时间 try: - error_text = response.text + if not error_text: + error_text = response.text cooldown_until = await parse_and_log_cooldown(error_text, mode="geminicli") except Exception: pass @@ -443,18 +520,6 @@ async def non_stream_request( # 不重试,直接返回原始错误 log.error(f"[NON-STREAM] 达到最大重试次数或不应重试,返回原始错误") return last_error_response - else: - # 错误码在禁用码当中,直接返回,无需重试 - try: - error_text = response.text - log.error(f"非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") - except Exception: - log.error(f"非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}") - await record_api_call_error( - credential_manager, current_file, status_code, - None, mode="geminicli", model_key=model_group - ) - return last_error_response except Exception as e: log.error(f"非流式请求异常: {e}, 凭证: {current_file}") From acfc5bac9eef480e727e9eb67694c5dbd9e4a5ac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 11:41:40 +0000 Subject: [PATCH 056/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index f22326e31..8dcbfa16b 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=26b050662577200323abcf2b961de9673b1d8fe6 -short_hash=26b0506 -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 19:10:04 +0800 +full_hash=d41cad935b056d5b4ebf1fbbc8c1bfd9d35fd4cb +short_hash=d41cad9 +message=修正重试逻辑 +date=2026-01-11 19:41:30 +0800 From a0090dc0db33204347f952e225849c32d9084d3c Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 20:01:07 +0800 Subject: [PATCH 057/211] =?UTF-8?q?=E4=BF=AE=E6=AD=A3gemini=E6=A0=BC?= =?UTF-8?q?=E5=BC=8Fmcp=E6=97=A0=E9=99=90=E8=B0=83=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/gemini_fix.py | 104 ++++-------------------------------- src/models.py | 14 ++--- 2 files changed, 18 insertions(+), 100 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index be8ea31b0..27880f0c2 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -10,85 +10,6 @@ # ==================== Gemini API 配置 ==================== -# Gemini API 不支持的 JSON Schema 字段集合 -# 参考: github.com/googleapis/python-genai/issues/699, #388, #460, #1122, #264, #4551 -UNSUPPORTED_SCHEMA_KEYS = { - '$schema', '$id', '$ref', '$defs', 'definitions', - 'example', 'examples', 'readOnly', 'writeOnly', 'default', - 'exclusiveMaximum', 'exclusiveMinimum', - 'oneOf', 'anyOf', 'allOf', 'const', - 'additionalItems', 'contains', 'patternProperties', 'dependencies', - 'propertyNames', 'if', 'then', 'else', - 'contentEncoding', 'contentMediaType', - 'additionalProperties', 'minLength', 'maxLength', - 'minItems', 'maxItems', 'uniqueItems' -} - - - -def clean_tools_for_gemini(tools: Optional[List[Dict[str, Any]]]) -> Optional[List[Dict[str, Any]]]: - """ - 清理工具定义,移除 Gemini API 不支持的 JSON Schema 字段 - - Gemini API 只支持有限的 OpenAPI 3.0 Schema 属性: - - 支持: type, description, enum, items, properties, required, nullable, format - - 不支持: $schema, $id, $ref, $defs, title, examples, default, readOnly, - exclusiveMaximum, exclusiveMinimum, oneOf, anyOf, allOf, const 等 - - Args: - tools: 工具定义列表 - - Returns: - 清理后的工具定义列表 - """ - if not tools: - return tools - - def clean_schema(obj: Any) -> Any: - """递归清理 schema 对象""" - if isinstance(obj, dict): - cleaned = {} - for key, value in obj.items(): - if key in UNSUPPORTED_SCHEMA_KEYS: - continue - cleaned[key] = clean_schema(value) - # 确保有 type 字段(如果有 properties 但没有 type) - if "properties" in cleaned and "type" not in cleaned: - cleaned["type"] = "object" - return cleaned - elif isinstance(obj, list): - return [clean_schema(item) for item in obj] - else: - return obj - - # 清理每个工具的参数 - cleaned_tools = [] - for tool in tools: - if not isinstance(tool, dict): - cleaned_tools.append(tool) - continue - - cleaned_tool = tool.copy() - - # 清理 functionDeclarations - if "functionDeclarations" in cleaned_tool: - cleaned_declarations = [] - for func_decl in cleaned_tool["functionDeclarations"]: - if not isinstance(func_decl, dict): - cleaned_declarations.append(func_decl) - continue - - cleaned_decl = func_decl.copy() - if "parameters" in cleaned_decl: - cleaned_decl["parameters"] = clean_schema(cleaned_decl["parameters"]) - cleaned_declarations.append(cleaned_decl) - - cleaned_tool["functionDeclarations"] = cleaned_declarations - - cleaned_tools.append(cleaned_tool) - - return cleaned_tools - def prepare_image_generation_request( request_body: Dict[str, Any], model: str @@ -276,18 +197,14 @@ async def normalize_gemini_request( "includeThoughts": final_include_thoughts } - # 2. 工具清理和处理 - if tools: - result["tools"] = clean_tools_for_gemini(tools) - - # 3. 搜索模型添加 Google Search + # 2. 搜索模型添加 Google Search if is_search_model(model): result_tools = result.get("tools") or [] result["tools"] = result_tools if not any(tool.get("googleSearch") for tool in result_tools if isinstance(tool, dict)): result_tools.append({"googleSearch": {}}) - # 4. 模型名称处理 + # 3. 模型名称处理 result["model"] = get_base_model_name(model) elif mode == "antigravity": @@ -365,10 +282,6 @@ async def normalize_gemini_request( if top_k is not None: generation_config["topK"] = 64 - # 3. 工具清理 - if tools: - result["tools"] = clean_tools_for_gemini(tools) - if "contents" in result: cleaned_contents = [] for content in result["contents"]: @@ -379,12 +292,15 @@ async def normalize_gemini_request( if not isinstance(part, dict): continue - # 检查 part 是否有有效的数据字段 - has_valid_data = any( - key in part and part[key] - for key in ["text", "inlineData", "fileData", "functionCall", "functionResponse"] + # 检查 part 是否有有效的非空值 + # 过滤掉空字典或所有值都为空的 part + has_valid_value = any( + value not in (None, "", {}, []) + for key, value in part.items() + if key != "thought" # thought 字段可以为空 ) - if has_valid_data: + + if has_valid_value: valid_parts.append(part) else: log.warning(f"[GEMINI_FIX] 移除空的或无效的 part: {part}") diff --git a/src/models.py b/src/models.py index 37b6ca119..c04d4b811 100644 --- a/src/models.py +++ b/src/models.py @@ -6,16 +6,16 @@ # Pydantic v1/v2 兼容性辅助函数 def model_to_dict(model: BaseModel) -> Dict[str, Any]: """ - 兼容 Pydantic v1 和 v2 的模型转字典方法 - - v1: model.dict() - - v2: model.model_dump() + 兼容 Pydantic v1 和 v2 的模型转字典方法,排除 None 值 + - v1: model.dict(exclude_none=True) + - v2: model.model_dump(exclude_none=True) """ if hasattr(model, 'model_dump'): # Pydantic v2 - return model.model_dump() + return model.model_dump(exclude_none=True) else: # Pydantic v1 - return model.dict() + return model.dict(exclude_none=True) # Common Models @@ -123,10 +123,12 @@ class OpenAIChatCompletionStreamResponse(BaseModel): # Gemini Models class GeminiPart(BaseModel): + model_config = {"extra": "allow"} # 允许额外字段(如 functionCall, functionResponse) + text: Optional[str] = None inlineData: Optional[Dict[str, Any]] = None fileData: Optional[Dict[str, Any]] = None - thought: Optional[bool] = False + thought: Optional[bool] = None # 改为 None,避免序列化时包含 False class GeminiContent(BaseModel): From 9a507edbc6ffddb050ada64c64238fe98aabadf3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 12:01:39 +0000 Subject: [PATCH 058/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 8dcbfa16b..70f16f6d8 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=d41cad935b056d5b4ebf1fbbc8c1bfd9d35fd4cb -short_hash=d41cad9 -message=修正重试逻辑 -date=2026-01-11 19:41:30 +0800 +full_hash=025d801b3a78c7ef1d35f3d4365487b53dda67fd +short_hash=025d801 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-11 20:01:25 +0800 From 6e182269d3503d3631d0c04458c4b3be9400976c Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 20:33:54 +0800 Subject: [PATCH 059/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 54 +++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 27880f0c2..b454c42b7 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -146,8 +146,8 @@ def check_last_assistant_has_thinking(contents: List[Dict[str, Any]]) -> bool: if not isinstance(first_part, dict): return False - # 检查是否是 thinking 块(有 thought 字段且为 True) - return first_part.get("thought") is True + # 检查是否是 thinking 块(有 thought_signature 字段) + return "thought_signature" in first_part async def normalize_gemini_request( @@ -229,28 +229,42 @@ async def normalize_gemini_request( else: # 3. 思考模型处理 if is_thinking_model(model): + # 直接设置 thinkingConfig + if "thinkingConfig" not in generation_config: + generation_config["thinkingConfig"] = {} + + thinking_config = generation_config["thinkingConfig"] + # 优先使用传入的思考预算,否则使用默认值 + if "thinkingBudget" not in thinking_config: + thinking_config["thinkingBudget"] = 1024 + if "includeThoughts" not in thinking_config: + thinking_config["includeThoughts"] = return_thoughts + # 检查最后一个 assistant 消息是否以 thinking 块开始 contents = result.get("contents", []) - can_enable_thinking = check_last_assistant_has_thinking(contents) - - if can_enable_thinking: - if "thinkingConfig" not in generation_config: - generation_config["thinkingConfig"] = {} + + if not check_last_assistant_has_thinking(contents): + # 最后一个 assistant 消息不是以 thinking 块开始,填充思考块避免失效 + log.warning(f"[ANTIGRAVITY] 最后一个 assistant 消息不以 thinking 块开始,自动填充思考块") - thinking_config = generation_config["thinkingConfig"] - # 优先使用传入的思考预算,否则使用默认值 - if "thinkingBudget" not in thinking_config: - thinking_config["thinkingBudget"] = 1024 - if "includeThoughts" not in thinking_config: - thinking_config["includeThoughts"] = return_thoughts - else: - # 最后一个 assistant 消息不是以 thinking 块开始,禁用 thinking - log.warning(f"[ANTIGRAVITY] 最后一个 assistant 消息不以 thinking 块开始,禁用 thinkingConfig") - # 移除可能存在的 thinkingConfig - generation_config.pop("thinkingConfig", None) + # 找到最后一个 model 角色的 content + for i in range(len(contents) - 1, -1, -1): + content = contents[i] + if isinstance(content, dict) and content.get("role") == "model": + # 在 parts 开头插入思考块(使用官方跳过验证的虚拟签名) + parts = content.get("parts", []) + thinking_part = { + "text": "Continuing from previous context...", + "thoughtSignature": "skip_thought_signature_validator" # 官方文档推荐的虚拟签名 + } + # 如果第一个 part 不是 thinking,则插入 + if not parts or not (isinstance(parts[0], dict) and "thoughtSignature" in parts[0]): + content["parts"] = [thinking_part] + parts + log.info(f"[ANTIGRAVITY] 已在最后一个 assistant 消息开头插入思考块(含跳过验证签名)") + break - # 移除 -thinking 后缀 - model = model.replace("-thinking", "") + # 移除 -thinking 后缀 + model = model.replace("-thinking", "") # 4. Claude 模型关键词映射 # 使用关键词匹配而不是精确匹配,更灵活地处理各种变体 From b0b550622d7765efbb8840db23cbef31679c730b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 12:34:05 +0000 Subject: [PATCH 060/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 70f16f6d8..5d8686cae 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=025d801b3a78c7ef1d35f3d4365487b53dda67fd -short_hash=025d801 -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-11 20:01:25 +0800 +full_hash=6e182269d3503d3631d0c04458c4b3be9400976c +short_hash=6e18226 +message=Update gemini_fix.py +date=2026-01-11 20:33:54 +0800 From 74ac3debd28870bb1e39201f5f50892aee51a1e1 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 20:35:50 +0800 Subject: [PATCH 061/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index b454c42b7..1944b3d5c 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -243,7 +243,7 @@ async def normalize_gemini_request( # 检查最后一个 assistant 消息是否以 thinking 块开始 contents = result.get("contents", []) - if not check_last_assistant_has_thinking(contents): + if not check_last_assistant_has_thinking(contents) and "claude" in model.lower(): # 最后一个 assistant 消息不是以 thinking 块开始,填充思考块避免失效 log.warning(f"[ANTIGRAVITY] 最后一个 assistant 消息不以 thinking 块开始,自动填充思考块") From a6c63e1caeb3e1d6e298b057df619229d4f21075 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 12:36:12 +0000 Subject: [PATCH 062/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 5d8686cae..eeffd8e23 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=6e182269d3503d3631d0c04458c4b3be9400976c -short_hash=6e18226 +full_hash=74ac3debd28870bb1e39201f5f50892aee51a1e1 +short_hash=74ac3de message=Update gemini_fix.py -date=2026-01-11 20:33:54 +0800 +date=2026-01-11 20:35:50 +0800 From 59e0eda0b3811655771ba0d3f5a81a48ae694551 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 20:39:31 +0800 Subject: [PATCH 063/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 1944b3d5c..735470f09 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -260,7 +260,7 @@ async def normalize_gemini_request( # 如果第一个 part 不是 thinking,则插入 if not parts or not (isinstance(parts[0], dict) and "thoughtSignature" in parts[0]): content["parts"] = [thinking_part] + parts - log.info(f"[ANTIGRAVITY] 已在最后一个 assistant 消息开头插入思考块(含跳过验证签名)") + log.debug(f"[ANTIGRAVITY] 已在最后一个 assistant 消息开头插入思考块(含跳过验证签名)") break # 移除 -thinking 后缀 From c2f181dfc39d1837bf85df776d327d490656a074 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 21:11:21 +0800 Subject: [PATCH 064/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 735470f09..db1976de0 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -315,6 +315,10 @@ async def normalize_gemini_request( ) if has_valid_value: + # 清理 text 字段的尾随空格 + if "text" in part and isinstance(part["text"], str): + part = part.copy() + part["text"] = part["text"].rstrip() valid_parts.append(part) else: log.warning(f"[GEMINI_FIX] 移除空的或无效的 part: {part}") From e2fc2d8a8b82af5ee1dc47849ead3ff8ad485e5b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 13:12:41 +0000 Subject: [PATCH 065/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index eeffd8e23..8828b8dfd 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=74ac3debd28870bb1e39201f5f50892aee51a1e1 -short_hash=74ac3de +full_hash=c2f181dfc39d1837bf85df776d327d490656a074 +short_hash=c2f181d message=Update gemini_fix.py -date=2026-01-11 20:35:50 +0800 +date=2026-01-11 21:11:21 +0800 From deae390d1b6a5a138a3b5a53655ac1e8ccae1668 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 22:35:29 +0800 Subject: [PATCH 066/211] Update models.py --- src/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/models.py b/src/models.py index c04d4b811..1e947a258 100644 --- a/src/models.py +++ b/src/models.py @@ -123,12 +123,13 @@ class OpenAIChatCompletionStreamResponse(BaseModel): # Gemini Models class GeminiPart(BaseModel): - model_config = {"extra": "allow"} # 允许额外字段(如 functionCall, functionResponse) - text: Optional[str] = None inlineData: Optional[Dict[str, Any]] = None fileData: Optional[Dict[str, Any]] = None thought: Optional[bool] = None # 改为 None,避免序列化时包含 False + + class Config: + extra = "allow" # 允许额外字段(如 functionCall, functionResponse) class GeminiContent(BaseModel): From 6abbd2b5115620852f23bc51da97f87ddb23d74f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 14:35:39 +0000 Subject: [PATCH 067/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 8828b8dfd..6560fcaf1 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=c2f181dfc39d1837bf85df776d327d490656a074 -short_hash=c2f181d -message=Update gemini_fix.py -date=2026-01-11 21:11:21 +0800 +full_hash=deae390d1b6a5a138a3b5a53655ac1e8ccae1668 +short_hash=deae390 +message=Update models.py +date=2026-01-11 22:35:29 +0800 From 1d4d23aa115b8f8daf1a86c181dca5ea893f7c28 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 11 Jan 2026 23:00:38 +0800 Subject: [PATCH 068/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index db1976de0..378f4b54d 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -273,6 +273,8 @@ async def normalize_gemini_request( model = "claude-opus-4-5-thinking" elif "sonnet" in model.lower() or "haiku" in model.lower(): model = "claude-sonnet-4-5-thinking" + elif "haiku" in model.lower(): + model = "gemini-2.5-flash" elif "claude" in model.lower(): # Claude 模型兜底:如果包含 claude 但不是 opus/sonnet/haiku model = "claude-sonnet-4-5-thinking" From 0a34396ff076c0c12e9099d947d63d3ce6f90c9b Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 00:20:48 +0800 Subject: [PATCH 069/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 378f4b54d..5823a4141 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -159,7 +159,6 @@ async def normalize_gemini_request( 处理逻辑: 1. 模型特性处理 (thinking config, search tools) - 2. 字段名转换 (system_instructions -> systemInstruction) 3. 参数范围限制 (maxOutputTokens, topK) 4. 工具清理 @@ -284,9 +283,6 @@ async def normalize_gemini_request( log.debug(f"[ANTIGRAVITY] 映射模型: {original_model} -> {model}") # ========== 公共处理 ========== - # 1. 字段名转换 - if "system_instructions" in result: - result["systemInstruction"] = result.pop("system_instructions") # 2. 参数范围限制 if generation_config: From 01587d9908111c72e7d46e2aa16303a2239ffcd1 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 00:40:11 +0800 Subject: [PATCH 070/211] Update anthropic2gemini.py --- src/converter/anthropic2gemini.py | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index d7f630b12..67fdcc4d5 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -647,9 +647,48 @@ def build_generation_config(payload: Dict[str, Any]) -> Dict[str, Any]: if max_tokens is not None: config["maxOutputTokens"] = max_tokens + # 处理 extended thinking 参数 (plan mode) + thinking = payload.get("thinking") + is_plan_mode = False + if thinking and isinstance(thinking, dict): + thinking_type = thinking.get("type") + budget_tokens = thinking.get("budget_tokens") + + # 如果启用了 extended thinking,设置 thinkingConfig + if thinking_type == "enabled": + is_plan_mode = True + thinking_config: Dict[str, Any] = {} + + # 设置思考预算,默认使用较大的值以支持计划模式 + if budget_tokens is not None: + thinking_config["thinkingBudget"] = budget_tokens + else: + # 默认给一个较大的思考预算以支持完整的计划生成 + thinking_config["thinkingBudget"] = 10240 + + # 始终包含思考内容,这样才能看到计划 + thinking_config["includeThoughts"] = True + + config["thinkingConfig"] = thinking_config + log.info(f"[ANTHROPIC2GEMINI] Extended thinking enabled with budget: {thinking_config['thinkingBudget']}") + elif thinking_type == "disabled": + # 明确禁用思考模式 + config["thinkingConfig"] = { + "includeThoughts": False + } + log.info("[ANTHROPIC2GEMINI] Extended thinking explicitly disabled") + stop_sequences = payload.get("stop_sequences") if isinstance(stop_sequences, list) and stop_sequences: config["stopSequences"] = config["stopSequences"] + [str(s) for s in stop_sequences] + elif is_plan_mode: + # Plan mode 时清空默认 stop sequences,避免过早停止 + # 默认的 stop sequences 可能会导致模型在生成计划时过早停止 + config["stopSequences"] = [] + log.info("[ANTHROPIC2GEMINI] Plan mode: cleared default stop sequences to prevent premature stopping") + + # 如果不是 plan mode 且没有自定义 stop_sequences,保持默认值 + # (默认值已经在 config 初始化时设置) return config From 1548cfb00cca794d73aeb687988b086e449fb9e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 16:40:48 +0000 Subject: [PATCH 071/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 6560fcaf1..77d545088 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=deae390d1b6a5a138a3b5a53655ac1e8ccae1668 -short_hash=deae390 -message=Update models.py -date=2026-01-11 22:35:29 +0800 +full_hash=01587d9908111c72e7d46e2aa16303a2239ffcd1 +short_hash=01587d9 +message=Update anthropic2gemini.py +date=2026-01-12 00:40:11 +0800 From ec6f647aaf04ce2879f21bf7888b5b024c76b312 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 01:20:58 +0800 Subject: [PATCH 072/211] Update antigravity.py --- src/api/antigravity.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index dee316240..16a86e180 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -75,7 +75,10 @@ def build_antigravity_headers(access_token: str, model_name: str = "") -> Dict[s # 根据模型名称判断 request_type if model_name: - request_type = "image_gen" if "image" in model_name.lower() else "agent" + if "image" in model_name.lower(): + request_type = "image_gen" + else: + request_type = "agent" headers['requestType'] = request_type return headers @@ -121,6 +124,7 @@ async def stream_request( current_file, credential_data = cred_result access_token = credential_data.get("access_token") or credential_data.get("token") + project_id = credential_data.get("project_id", "") if not access_token: log.error(f"[ANTIGRAVITY STREAM] No access token in credential: {current_file}") @@ -141,6 +145,13 @@ async def stream_request( if headers: auth_headers.update(headers) + # 构建包含project的payload + final_payload = { + "model": body.get("model"), + "project": project_id, + "request": body.get("request", {}), + } + # 3. 调用stream_post_async进行请求 retry_config = await get_retry_config() max_retries = retry_config["max_retries"] @@ -151,7 +162,7 @@ async def stream_request( # 内部函数:获取新凭证并更新headers async def refresh_credential(): - nonlocal current_file, access_token, auth_headers + nonlocal current_file, access_token, auth_headers, project_id, final_payload cred_result = await credential_manager.get_valid_credential( mode="antigravity", model_key=model_name ) @@ -159,11 +170,13 @@ async def refresh_credential(): return None current_file, credential_data = cred_result access_token = credential_data.get("access_token") or credential_data.get("token") + project_id = credential_data.get("project_id", "") if not access_token: return None auth_headers = build_antigravity_headers(access_token, model_name) if headers: auth_headers.update(headers) + final_payload = {"model": body.get("model"), "project": project_id, "request": body.get("request", {})} return True for attempt in range(max_retries + 1): @@ -173,7 +186,7 @@ async def refresh_credential(): try: async for chunk in stream_post_async( url=target_url, - body=body, + body=final_payload, native=native, headers=auth_headers ): @@ -352,6 +365,7 @@ async def non_stream_request( current_file, credential_data = cred_result access_token = credential_data.get("access_token") or credential_data.get("token") + project_id = credential_data.get("project_id", "") if not access_token: log.error(f"[ANTIGRAVITY] No access token in credential: {current_file}") @@ -371,6 +385,13 @@ async def non_stream_request( if headers: auth_headers.update(headers) + # 构建包含project的payload + final_payload = { + "model": body.get("model"), + "project": project_id, + "request": body.get("request", {}), + } + # 3. 调用post_async进行请求 retry_config = await get_retry_config() max_retries = retry_config["max_retries"] @@ -381,7 +402,7 @@ async def non_stream_request( # 内部函数:获取新凭证并更新headers async def refresh_credential(): - nonlocal current_file, access_token, auth_headers + nonlocal current_file, access_token, auth_headers, project_id, final_payload cred_result = await credential_manager.get_valid_credential( mode="antigravity", model_key=model_name ) @@ -389,11 +410,13 @@ async def refresh_credential(): return None current_file, credential_data = cred_result access_token = credential_data.get("access_token") or credential_data.get("token") + project_id = credential_data.get("project_id", "") if not access_token: return None auth_headers = build_antigravity_headers(access_token, model_name) if headers: auth_headers.update(headers) + final_payload = {"model": body.get("model"), "project": project_id, "request": body.get("request", {})} return True for attempt in range(max_retries + 1): @@ -402,7 +425,7 @@ async def refresh_credential(): try: response = await post_async( url=target_url, - json=body, + json=final_payload, headers=auth_headers, timeout=300.0 ) From dfad9369f0a069d43327af3187eebc3d87a0de1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 Jan 2026 17:21:16 +0000 Subject: [PATCH 073/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 77d545088..799a8a413 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=01587d9908111c72e7d46e2aa16303a2239ffcd1 -short_hash=01587d9 -message=Update anthropic2gemini.py -date=2026-01-12 00:40:11 +0800 +full_hash=c991b85a8603e457af727d05ddb846d912ff26e5 +short_hash=c991b85 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-12 01:21:01 +0800 From a6d20660cb62af46d4c85fab9d93dd2cc9efb05f Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 12:03:37 +0800 Subject: [PATCH 074/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 5823a4141..02e3635ce 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -227,7 +227,7 @@ async def normalize_gemini_request( return prepare_image_generation_request(result, model) else: # 3. 思考模型处理 - if is_thinking_model(model): + if is_thinking_model(model) or ("thinkingBudget" in generation_config.get("thinkingConfig", {}) and generation_config["thinkingConfig"]["thinkingBudget"] != 0): # 直接设置 thinkingConfig if "thinkingConfig" not in generation_config: generation_config["thinkingConfig"] = {} @@ -236,8 +236,7 @@ async def normalize_gemini_request( # 优先使用传入的思考预算,否则使用默认值 if "thinkingBudget" not in thinking_config: thinking_config["thinkingBudget"] = 1024 - if "includeThoughts" not in thinking_config: - thinking_config["includeThoughts"] = return_thoughts + thinking_config["includeThoughts"] = return_thoughts # 检查最后一个 assistant 消息是否以 thinking 块开始 contents = result.get("contents", []) From 8ac2f5312183a6bca19b436f807d5b2e0a153a7c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 04:07:13 +0000 Subject: [PATCH 075/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 799a8a413..f51e362bd 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=c991b85a8603e457af727d05ddb846d912ff26e5 -short_hash=c991b85 +full_hash=d4b7a49cd28dd7a408fa5004093806bf68f8f9bf +short_hash=d4b7a49 message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-12 01:21:01 +0800 +date=2026-01-12 12:04:07 +0800 From 511bbca723d2e6a92606e33d900f05135f96b434 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 13:06:31 +0800 Subject: [PATCH 076/211] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 21 ++++- src/converter/gemini_fix.py | 54 ++++++++----- src/converter/openai2gemini.py | 126 ++++++++++++++++-------------- 3 files changed, 121 insertions(+), 80 deletions(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index 67fdcc4d5..a0c5ea718 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -874,9 +874,16 @@ def gemini_to_anthropic_response( # 确定停止原因 finish_reason = candidate.get("finishReason") - stop_reason = "tool_use" if has_tool_use else "end_turn" - if finish_reason == "MAX_TOKENS" and not has_tool_use: + + # 只有在正常停止(STOP)且有工具调用时才设为 tool_use + # 避免在 SAFETY、MAX_TOKENS 等情况下仍然返回 tool_use 导致循环 + if has_tool_use and finish_reason == "STOP": + stop_reason = "tool_use" + elif finish_reason == "MAX_TOKENS": stop_reason = "max_tokens" + else: + # 其他情况(SAFETY、RECITATION 等)默认为 end_turn + stop_reason = "end_turn" # 提取 token 使用情况 input_tokens = usage_metadata.get("promptTokenCount", 0) if isinstance(usage_metadata, dict) else 0 @@ -1185,9 +1192,15 @@ def _close_block() -> Optional[bytes]: yield close_evt # 确定停止原因 - stop_reason = "tool_use" if has_tool_use else "end_turn" - if finish_reason == "MAX_TOKENS" and not has_tool_use: + # 只有在正常停止(STOP)且有工具调用时才设为 tool_use + # 避免在 SAFETY、MAX_TOKENS 等情况下仍然返回 tool_use 导致循环 + if has_tool_use and finish_reason == "STOP": + stop_reason = "tool_use" + elif finish_reason == "MAX_TOKENS": stop_reason = "max_tokens" + else: + # 其他情况(SAFETY、RECITATION 等)默认为 end_turn + stop_reason = "end_turn" if _anthropic_debug_enabled(): log.info( diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 02e3635ce..458afd047 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -146,8 +146,8 @@ def check_last_assistant_has_thinking(contents: List[Dict[str, Any]]) -> bool: if not isinstance(first_part, dict): return False - # 检查是否是 thinking 块(有 thought_signature 字段) - return "thought_signature" in first_part + # 检查是否是 thinking 块(有 thought 或 thoughtSignature 字段) + return "thought" in first_part or "thoughtSignature" in first_part async def normalize_gemini_request( @@ -242,24 +242,40 @@ async def normalize_gemini_request( contents = result.get("contents", []) if not check_last_assistant_has_thinking(contents) and "claude" in model.lower(): - # 最后一个 assistant 消息不是以 thinking 块开始,填充思考块避免失效 - log.warning(f"[ANTIGRAVITY] 最后一个 assistant 消息不以 thinking 块开始,自动填充思考块") + # 检测是否有工具调用(MCP场景) + has_tool_calls = any( + isinstance(content, dict) and + any( + isinstance(part, dict) and ("functionCall" in part or "function_call" in part) + for part in content.get("parts", []) + ) + for content in contents + ) - # 找到最后一个 model 角色的 content - for i in range(len(contents) - 1, -1, -1): - content = contents[i] - if isinstance(content, dict) and content.get("role") == "model": - # 在 parts 开头插入思考块(使用官方跳过验证的虚拟签名) - parts = content.get("parts", []) - thinking_part = { - "text": "Continuing from previous context...", - "thoughtSignature": "skip_thought_signature_validator" # 官方文档推荐的虚拟签名 - } - # 如果第一个 part 不是 thinking,则插入 - if not parts or not (isinstance(parts[0], dict) and "thoughtSignature" in parts[0]): - content["parts"] = [thinking_part] + parts - log.debug(f"[ANTIGRAVITY] 已在最后一个 assistant 消息开头插入思考块(含跳过验证签名)") - break + if has_tool_calls: + # MCP 场景:检测到工具调用,移除 thinkingConfig + log.warning(f"[ANTIGRAVITY] 检测到工具调用(MCP场景),移除 thinkingConfig 避免失效") + generation_config.pop("thinkingConfig", None) + else: + # 非 MCP 场景:填充思考块 + log.warning(f"[ANTIGRAVITY] 最后一个 assistant 消息不以 thinking 块开始,自动填充思考块") + + # 找到最后一个 model 角色的 content + for i in range(len(contents) - 1, -1, -1): + content = contents[i] + if isinstance(content, dict) and content.get("role") == "model": + # 在 parts 开头插入思考块(使用官方跳过验证的虚拟签名) + parts = content.get("parts", []) + thinking_part = { + "text": "Continuing from previous context...", + # "thought": True, # 标记为思考块 + "thoughtSignature": "skip_thought_signature_validator" # 官方文档推荐的虚拟签名 + } + # 如果第一个 part 不是 thinking,则插入 + if not parts or not (isinstance(parts[0], dict) and ("thought" in parts[0] or "thoughtSignature" in parts[0])): + content["parts"] = [thinking_part] + parts + log.debug(f"[ANTIGRAVITY] 已在最后一个 assistant 消息开头插入思考块(含跳过验证签名)") + break # 移除 -thinking 后缀 model = model.replace("-thinking", "") diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 38c02f7eb..c8a6222fa 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -66,7 +66,9 @@ def _map_finish_reason(gemini_reason: str) -> str: elif gemini_reason in ["SAFETY", "RECITATION"]: return "content_filter" else: - return None + # 对于 None 或未知的 finishReason,返回 "stop" 作为默认值 + # 避免返回 None 导致 MCP 客户端误判为响应未完成而循环调用 + return "stop" # ==================== Tool Conversion Functions ==================== @@ -78,15 +80,14 @@ def _normalize_function_name(name: str) -> str: 规则: - 必须以字母或下划线开头 - - 只能包含 a-z, A-Z, 0-9, 下划线, 点, 短横线 + - 只能包含 a-z, A-Z, 0-9, 下划线, 英文句点, 英文短划线 - 最大长度 64 个字符 转换策略: - - 中文字符转换为拼音 - - 如果以非字母/下划线开头,添加 "_" 前缀 - - 将非法字符(空格、@、#等)替换为下划线 - - 连续的下划线合并为一个 - - 如果超过 64 个字符,截断 + 1. 中文字符转换为拼音 + 2. 将非法字符替换为下划线 + 3. 如果以非字母/下划线开头,添加下划线前缀 + 4. 截断到 64 个字符 Args: name: 原始函数名 @@ -99,20 +100,16 @@ def _normalize_function_name(name: str) -> str: if not name: return "_unnamed_function" - # 第零步:检测并转换中文字符为拼音 - # 检查是否包含中文字符 + # 步骤1:转换中文字符为拼音 if re.search(r"[\u4e00-\u9fff]", name): try: - - # 将中文转换为拼音,用下划线连接多音字 parts = [] for char in name: if "\u4e00" <= char <= "\u9fff": - # 中文字符,转换为拼音 + # 中文字符转换为拼音 pinyin = lazy_pinyin(char, style=Style.NORMAL) parts.append("".join(pinyin)) else: - # 非中文字符,保持不变 parts.append(char) normalized = "".join(parts) except ImportError: @@ -121,41 +118,23 @@ def _normalize_function_name(name: str) -> str: else: normalized = name - # 第一步:将非法字符替换为下划线 - # 保留:a-z, A-Z, 0-9, 下划线, 点, 短横线 + # 步骤2:将非法字符替换为下划线 + # 合法字符:a-z, A-Z, 0-9, _, ., - normalized = re.sub(r"[^a-zA-Z0-9_.\-]", "_", normalized) - # 第二步:如果以非字母/下划线开头,处理首字符 - prefix_added = False + # 步骤3:确保以字母或下划线开头 if normalized and not (normalized[0].isalpha() or normalized[0] == "_"): - if normalized[0] in ".-": - # 点和短横线在开头位置替换为下划线(它们在中间是合法的) - normalized = "_" + normalized[1:] - else: - # 其他字符(如数字)添加下划线前缀 - normalized = "_" + normalized - prefix_added = True - - # 第三步:合并连续的下划线 - normalized = re.sub(r"_+", "_", normalized) - - # 第四步:移除首尾的下划线 - # 如果原本就是下划线开头,或者我们添加了前缀,则保留开头的下划线 - if name.startswith("_") or prefix_added: - # 只移除尾部的下划线 - normalized = normalized.rstrip("_") - else: - # 移除首尾的下划线 - normalized = normalized.strip("_") + # 以数字、点或短横线开头,添加下划线前缀 + normalized = "_" + normalized - # 第五步:确保不为空 - if not normalized: - normalized = "_unnamed_function" - - # 第六步:截断到 64 个字符 + # 步骤4:截断到 64 个字符 if len(normalized) > 64: normalized = normalized[:64] + # 步骤5:确保不为空 + if not normalized: + normalized = "_unnamed_function" + return normalized @@ -797,6 +776,18 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di # 提取消息列表 messages = openai_request.get("messages", []) + # 构建 tool_call_id -> (name, original_id, signature) 的映射 + tool_call_mapping = {} + for msg in messages: + if msg.get("role") == "assistant" and msg.get("tool_calls"): + for tc in msg["tool_calls"]: + encoded_id = tc.get("id", "") + func_name = tc.get("function", {}).get("name") + if encoded_id and func_name: + # 解码获取原始ID和签名 + original_id, signature = decode_tool_id_and_signature(encoded_id) + tool_call_mapping[encoded_id] = (func_name, original_id, signature) + # 构建工具名称到参数 schema 的映射(用于类型修正) tool_schemas = {} if "tools" in openai_request and openai_request["tools"]: @@ -816,19 +807,26 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di tool_call_id = message.get("tool_call_id", "") func_name = message.get("name") - # 如果没有name,尝试从消息列表中查找 - if not func_name and tool_call_id: - for msg in messages: - if msg.get("role") == "assistant" and msg.get("tool_calls"): - for tc in msg["tool_calls"]: - if tc.get("id") == tool_call_id: - func_name = tc.get("function", {}).get("name") + # 使用映射表查找 + if tool_call_id in tool_call_mapping: + func_name, original_id, _ = tool_call_mapping[tool_call_id] + else: + # 如果没有name,尝试从消息列表中查找 + if not func_name and tool_call_id: + for msg in messages: + if msg.get("role") == "assistant" and msg.get("tool_calls"): + for tc in msg["tool_calls"]: + if tc.get("id") == tool_call_id: + func_name = tc.get("function", {}).get("name") + break + if func_name: break - if func_name: - break - if not func_name: - func_name = "unknown_function" + if not func_name: + func_name = "unknown_function" + + # 解码 tool_call_id 获取原始 ID + original_id, _ = decode_tool_id_and_signature(tool_call_id) # 解析响应数据 try: @@ -836,11 +834,12 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di except (json.JSONDecodeError, TypeError): response_data = {"result": str(content)} + # 使用原始 ID(不带签名) contents.append({ "role": "user", "parts": [{ "functionResponse": { - "id": tool_call_id, + "id": original_id, "name": func_name, "response": response_data } @@ -1130,14 +1129,22 @@ def convert_gemini_to_openai_response( # 构建消息对象 message = {"role": role} + # 获取 Gemini 的 finishReason + gemini_finish_reason = candidate.get("finishReason") + # 如果有工具调用 if tool_calls: message["tool_calls"] = tool_calls message["content"] = text_content if text_content else None - finish_reason = "tool_calls" + # 只有在正常停止(STOP)时才设为 tool_calls,其他情况保持原始 finish_reason + # 这样可以避免在 SAFETY、MAX_TOKENS 等情况下仍然返回 tool_calls 导致循环 + if gemini_finish_reason == "STOP": + finish_reason = "tool_calls" + else: + finish_reason = _map_finish_reason(gemini_finish_reason) else: message["content"] = text_content - finish_reason = _map_finish_reason(candidate.get("finishReason")) + finish_reason = _map_finish_reason(gemini_finish_reason) # 添加 reasoning content (如果有) if reasoning_content: @@ -1300,8 +1307,13 @@ def convert_gemini_to_openai_stream( if reasoning_content: delta["reasoning_content"] = reasoning_content - finish_reason = _map_finish_reason(candidate.get("finishReason")) - if finish_reason and tool_calls: + # 获取 Gemini 的 finishReason + gemini_finish_reason = candidate.get("finishReason") + finish_reason = _map_finish_reason(gemini_finish_reason) + + # 只有在正常停止(STOP)且有工具调用时才设为 tool_calls + # 避免在 SAFETY、MAX_TOKENS 等情况下仍然返回 tool_calls 导致循环 + if tool_calls and gemini_finish_reason == "STOP": finish_reason = "tool_calls" choices.append({ From d01c35243e3289017d0166ca1b0ba4a0488829b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 05:07:24 +0000 Subject: [PATCH 077/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index f51e362bd..3442314e4 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=d4b7a49cd28dd7a408fa5004093806bf68f8f9bf -short_hash=d4b7a49 +full_hash=2a9ad90f791ec1b54a55dd4bcad6b72bbbc8b8a9 +short_hash=2a9ad90 message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-12 12:04:07 +0800 +date=2026-01-12 13:06:34 +0800 From 3c5303c1e6f8df24d39602b1a91ba915ee7c6ff4 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 13:23:19 +0800 Subject: [PATCH 078/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 458afd047..85f18b38b 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -109,7 +109,7 @@ def is_search_model(model_name: str) -> bool: def is_thinking_model(model_name: str) -> bool: """检查是否为思考模型 (包含 -thinking 或 pro)""" - return "-thinking" in model_name or "pro" in model_name.lower() + return "think" in model_name or "pro" in model_name.lower() def check_last_assistant_has_thinking(contents: List[Dict[str, Any]]) -> bool: @@ -187,14 +187,17 @@ async def normalize_gemini_request( # ========== 模式特定处理 ========== if mode == "geminicli": # 1. 思考设置 - thinking_budget, include_thoughts = get_thinking_settings(model) - if thinking_budget is not None and "thinkingConfig" not in generation_config: - # 如果配置为不返回thoughts,则强制设置为False;否则使用模型默认设置 - final_include_thoughts = include_thoughts if return_thoughts else False - generation_config["thinkingConfig"] = { - "thinkingBudget": thinking_budget, - "includeThoughts": final_include_thoughts - } + # 优先使用 get_thinking_settings 获取的思考预算 + thinking_budget = get_thinking_settings(model) + + # 其次使用传入的思考预算 + if thinking_budget is None: + thinking_budget = generation_config.get("thinkingConfig", {}).get("thinkingBudget") + + # 假如 is_thinking_model 为真或者思考预算不为0,设置 thinkingConfig + if is_thinking_model(model) or (thinking_budget and thinking_budget != 0): + # includeThoughts 使用配置值 + generation_config["thinkingConfig"]["includeThoughts"] = return_thoughts # 2. 搜索模型添加 Google Search if is_search_model(model): From 014300855ce8f870bc4aa70e6011f584c768f9b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 05:23:35 +0000 Subject: [PATCH 079/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 3442314e4..e76f92662 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=2a9ad90f791ec1b54a55dd4bcad6b72bbbc8b8a9 -short_hash=2a9ad90 +full_hash=d48686e14f73562091a44f0e5b4e0eb257ec53b7 +short_hash=d48686e message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-12 13:06:34 +0800 +date=2026-01-12 13:23:21 +0800 From 342abaa7217e5ab094028c99f0340b2d5ee2fd0a Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 14:04:50 +0800 Subject: [PATCH 080/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 85f18b38b..ea0f78794 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -188,14 +188,21 @@ async def normalize_gemini_request( if mode == "geminicli": # 1. 思考设置 # 优先使用 get_thinking_settings 获取的思考预算 - thinking_budget = get_thinking_settings(model) + thinking_budget, _ = get_thinking_settings(model) # 其次使用传入的思考预算 if thinking_budget is None: thinking_budget = generation_config.get("thinkingConfig", {}).get("thinkingBudget") # 假如 is_thinking_model 为真或者思考预算不为0,设置 thinkingConfig - if is_thinking_model(model) or (thinking_budget and thinking_budget != 0): + if is_thinking_model(model) or (thinking_budget and thinking_budget != 0): + if "thinkingConfig" not in generation_config: + generation_config["thinkingConfig"] = {} + + # 设置思考预算 + if thinking_budget: + generation_config["thinkingConfig"]["thinkingBudget"] = thinking_budget + # includeThoughts 使用配置值 generation_config["thinkingConfig"]["includeThoughts"] = return_thoughts From cf5a378aee13bbdfc65193354882cc6e739553ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 06:05:03 +0000 Subject: [PATCH 081/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index e76f92662..51eaebdd5 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=d48686e14f73562091a44f0e5b4e0eb257ec53b7 -short_hash=d48686e -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-12 13:23:21 +0800 +full_hash=342abaa7217e5ab094028c99f0340b2d5ee2fd0a +short_hash=342abaa +message=Update gemini_fix.py +date=2026-01-12 14:04:50 +0800 From 6bd843709ff907aa892e43333481d6c7ec0ea78e Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 16:23:18 +0800 Subject: [PATCH 082/211] Update anthropic2gemini.py --- src/converter/anthropic2gemini.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index a0c5ea718..a69525c68 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -664,7 +664,7 @@ def build_generation_config(payload: Dict[str, Any]) -> Dict[str, Any]: thinking_config["thinkingBudget"] = budget_tokens else: # 默认给一个较大的思考预算以支持完整的计划生成 - thinking_config["thinkingBudget"] = 10240 + thinking_config["thinkingBudget"] = 48000 # 始终包含思考内容,这样才能看到计划 thinking_config["includeThoughts"] = True From db4549eaf808cc05ad9920582b5bf06822568f1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 08:23:29 +0000 Subject: [PATCH 083/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 51eaebdd5..c81703652 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=342abaa7217e5ab094028c99f0340b2d5ee2fd0a -short_hash=342abaa -message=Update gemini_fix.py -date=2026-01-12 14:04:50 +0800 +full_hash=6bd843709ff907aa892e43333481d6c7ec0ea78e +short_hash=6bd8437 +message=Update anthropic2gemini.py +date=2026-01-12 16:23:18 +0800 From ca8e01f723c635f197310ddc7a1ef08b495dd9e0 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 19:03:02 +0800 Subject: [PATCH 084/211] Update credential_manager.py --- src/credential_manager.py | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/credential_manager.py b/src/credential_manager.py index ab86732cf..2fbcfc791 100644 --- a/src/credential_manager.py +++ b/src/credential_manager.py @@ -2,7 +2,6 @@ 凭证管理器 """ -import asyncio import time from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple @@ -24,7 +23,7 @@ def __init__(self): self._storage_adapter = None # 并发控制(简化) - self._operation_lock = asyncio.Lock() + # 后端数据库自行处理并发,credential_manager 不再使用本地锁 async def _ensure_initialized(self): """确保管理器已初始化(内部使用)""" @@ -33,13 +32,12 @@ async def _ensure_initialized(self): async def initialize(self): """初始化凭证管理器""" - async with self._operation_lock: - if self._initialized and self._storage_adapter is not None: - return + if self._initialized and self._storage_adapter is not None: + return - # 初始化统一存储适配器 - self._storage_adapter = await get_storage_adapter() - self._initialized = True + # 初始化统一存储适配器 + self._storage_adapter = await get_storage_adapter() + self._initialized = True async def close(self): """清理资源""" @@ -106,9 +104,8 @@ async def add_credential(self, credential_name: str, credential_data: Dict[str, 存储层会自动处理轮换顺序 """ await self._ensure_initialized() - async with self._operation_lock: - await self._storage_adapter.store_credential(credential_name, credential_data) - log.info(f"Credential added/updated: {credential_name}") + await self._storage_adapter.store_credential(credential_name, credential_data) + log.info(f"Credential added/updated: {credential_name}") async def add_antigravity_credential(self, credential_name: str, credential_data: Dict[str, Any]): """ @@ -116,21 +113,19 @@ async def add_antigravity_credential(self, credential_name: str, credential_data 存储层会自动处理轮换顺序 """ await self._ensure_initialized() - async with self._operation_lock: - await self._storage_adapter.store_credential(credential_name, credential_data, mode="antigravity") - log.info(f"Antigravity credential added/updated: {credential_name}") + await self._storage_adapter.store_credential(credential_name, credential_data, mode="antigravity") + log.info(f"Antigravity credential added/updated: {credential_name}") async def remove_credential(self, credential_name: str, mode: str = "geminicli") -> bool: """删除一个凭证""" await self._ensure_initialized() - async with self._operation_lock: - try: - await self._storage_adapter.delete_credential(credential_name, mode=mode) - log.info(f"Credential removed: {credential_name} (mode={mode})") - return True - except Exception as e: - log.error(f"Error removing credential {credential_name}: {e}") - return False + try: + await self._storage_adapter.delete_credential(credential_name, mode=mode) + log.info(f"Credential removed: {credential_name} (mode={mode})") + return True + except Exception as e: + log.error(f"Error removing credential {credential_name}: {e}") + return False async def update_credential_state(self, credential_name: str, state_updates: Dict[str, Any], mode: str = "geminicli"): """更新凭证状态""" From aa4fe03312cd80e0451a7257a45f5db10cadc397 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 19:34:51 +0800 Subject: [PATCH 085/211] =?UTF-8?q?=E5=85=A8=E5=B1=80=E5=8D=95=E5=88=97?= =?UTF-8?q?=E5=87=AD=E8=AF=81=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 26 ++------------------------ src/api/geminicli.py | 27 ++++----------------------- src/credential_manager.py | 37 ++++++++++++++++++++++++++++--------- src/web_routes.py | 20 +++----------------- web.py | 19 +++++-------------- 5 files changed, 42 insertions(+), 87 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 16a86e180..77007c003 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -17,7 +17,7 @@ ) from log import log -from src.credential_manager import CredentialManager +from src.credential_manager import credential_manager from src.httpx_client import stream_post_async, post_async from src.models import Model, model_to_dict from src.utils import ANTIGRAVITY_USER_AGENT @@ -34,22 +34,7 @@ # ==================== 全局凭证管理器 ==================== -# 全局凭证管理器实例(单例模式) -_credential_manager: Optional[CredentialManager] = None - - -async def _get_credential_manager() -> CredentialManager: - """ - 获取全局凭证管理器实例 - - Returns: - CredentialManager实例 - """ - global _credential_manager - if not _credential_manager: - _credential_manager = CredentialManager() - await _credential_manager.initialize() - return _credential_manager +# 使用全局单例 credential_manager,自动初始化 # ==================== 辅助函数 ==================== @@ -102,9 +87,6 @@ async def stream_request( Yields: Response对象(错误时)或 bytes流/str流(成功时) """ - # 获取凭证管理器 - credential_manager = await _get_credential_manager() - model_name = body.get("model", "") # 1. 获取有效凭证 @@ -344,9 +326,6 @@ async def non_stream_request( # 否则使用传统非流式模式 log.info("[ANTIGRAVITY] 使用传统非流式模式") - # 获取凭证管理器 - credential_manager = await _get_credential_manager() - model_name = body.get("model", "") # 1. 获取有效凭证 @@ -564,7 +543,6 @@ async def fetch_available_models() -> List[Dict[str, Any]]: 返回空列表如果获取失败 """ # 获取凭证管理器和可用凭证 - credential_manager = await _get_credential_manager() cred_result = await credential_manager.get_valid_credential(mode="antigravity") if not cred_result: log.error("[ANTIGRAVITY] No valid credentials available for fetching models") diff --git a/src/api/geminicli.py b/src/api/geminicli.py index fa40b23b8..533d8fac2 100644 --- a/src/api/geminicli.py +++ b/src/api/geminicli.py @@ -22,7 +22,7 @@ from src.api.utils import get_model_group from log import log -from src.credential_manager import CredentialManager +from src.credential_manager import credential_manager from src.httpx_client import stream_post_async, post_async # 导入共同的基础功能 @@ -37,22 +37,7 @@ # ==================== 全局凭证管理器 ==================== -# 全局凭证管理器实例(单例模式) -_credential_manager: Optional[CredentialManager] = None - - -async def _get_credential_manager() -> CredentialManager: - """ - 获取全局凭证管理器实例 - - Returns: - CredentialManager实例 - """ - global _credential_manager - if not _credential_manager: - _credential_manager = CredentialManager() - await _credential_manager.initialize() - return _credential_manager +# 使用全局单例 credential_manager,自动初始化 # ==================== 请求准备 ==================== @@ -116,9 +101,7 @@ async def stream_request( Yields: Response对象(错误时)或 bytes流/str流(成功时) """ - # 获取凭证管理器 - credential_manager = await _get_credential_manager() - + # 获取有效凭证 model_name = body.get("model", "") model_group = get_model_group(model_name) @@ -331,9 +314,7 @@ async def non_stream_request( Returns: Response对象 """ - # 获取凭证管理器 - credential_manager = await _get_credential_manager() - + # 获取有效凭证 model_name = body.get("model", "") model_group = get_model_group(model_name) diff --git a/src/credential_manager.py b/src/credential_manager.py index 2fbcfc791..b4286937f 100644 --- a/src/credential_manager.py +++ b/src/credential_manager.py @@ -501,16 +501,35 @@ def _is_permanent_refresh_failure(self, error_msg: str, status_code: Optional[in log.debug("未匹配到明确的永久失效模式,判定为临时错误") return False -# 全局实例管理(保持兼容性) -_credential_manager: Optional[CredentialManager] = None +class _CredentialManagerSingleton: + """单例包装器,支持懒加载和自动初始化""" + _instance: Optional[CredentialManager] = None + _lock = None -async def get_credential_manager() -> CredentialManager: - """获取全局凭证管理器实例""" - global _credential_manager + def __init__(self): + self._manager = None + + async def _get_or_create(self) -> CredentialManager: + """获取或创建单例实例(线程安全)""" + if self._instance is None: + # 简单的实例创建(异步环境下一般不需要复杂的锁) + if self._instance is None: + self._instance = CredentialManager() + await self._instance.initialize() + log.debug("CredentialManager singleton initialized") + + return self._instance + + def __getattr__(self, name): + """代理所有方法调用到真实的 CredentialManager 实例""" + async def _async_wrapper(*args, **kwargs): + manager = await self._get_or_create() + method = getattr(manager, name) + return await method(*args, **kwargs) + + return _async_wrapper - if _credential_manager is None: - _credential_manager = CredentialManager() - await _credential_manager.initialize() - return _credential_manager +# 全局单例实例 - 直接导入即可使用 +credential_manager = _CredentialManagerSingleton() diff --git a/src/web_routes.py b/src/web_routes.py index 67b34dbab..271dd347a 100644 --- a/src/web_routes.py +++ b/src/web_routes.py @@ -36,7 +36,7 @@ get_auth_status, verify_password, ) -from src.credential_manager import CredentialManager +from src.credential_manager import credential_manager from .models import ( LoginRequest, AuthStartRequest, @@ -55,8 +55,8 @@ # 创建路由器 router = APIRouter() -# 创建credential manager实例(延迟初始化,在首次使用时自动初始化) -credential_manager = CredentialManager() +# 不在模块级创建实例,使用单例工厂按需获取 +# 直接按需从模块工厂获取凭证管理器,避免与 web.py 产生循环导入 # WebSocket连接管理 @@ -140,20 +140,6 @@ def cleanup_dead_connections(self): manager = ConnectionManager() -async def ensure_credential_manager_initialized(): - """确保credential manager已初始化""" - if not credential_manager._initialized: - await credential_manager.initialize() - - -async def get_credential_manager(): - """获取全局凭证管理器实例(已废弃,直接使用模块级的 credential_manager)""" - global credential_manager - # 确保已初始化(在首次使用时自动初始化) - await credential_manager._ensure_initialized() - return credential_manager - - def is_mobile_user_agent(user_agent: str) -> bool: """检测是否为移动设备用户代理""" if not user_agent: diff --git a/web.py b/web.py index c9b3d45c5..d1b348238 100644 --- a/web.py +++ b/web.py @@ -14,7 +14,7 @@ from log import log # Import managers and utilities -from src.credential_manager import CredentialManager +from src.credential_manager import credential_manager # Import all routers from src.router.antigravity.openai import router as antigravity_openai_router @@ -47,10 +47,11 @@ async def lifespan(app: FastAPI): except Exception as e: log.error(f"配置缓存初始化失败: {e}") - # 初始化全局凭证管理器 + # 初始化全局凭证管理器(通过单例工厂) try: - global_credential_manager = CredentialManager() - await global_credential_manager.initialize() + # credential_manager 会在第一次调用时自动初始化 + # 这里预先触发初始化以便在启动时检测错误 + await credential_manager._get_or_create() log.info("凭证管理器初始化成功") except Exception as e: log.error(f"凭证管理器初始化失败: {e}") @@ -138,16 +139,6 @@ async def lifespan(app: FastAPI): async def keepalive() -> Response: return Response(status_code=200) - -def get_credential_manager(): - """获取全局凭证管理器实例""" - return global_credential_manager - - -# 导出给其他模块使用 -__all__ = ["app", "get_credential_manager"] - - async def main(): """异步主启动函数""" from hypercorn.asyncio import serve From 8101d14e1b1851b80a79b3a900332bdf01a7d5ae Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 19:37:52 +0800 Subject: [PATCH 086/211] =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/credential_manager.py | 4 ++-- src/google_oauth_api.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/credential_manager.py b/src/credential_manager.py index b4286937f..fcb245151 100644 --- a/src/credential_manager.py +++ b/src/credential_manager.py @@ -8,8 +8,8 @@ from log import log -from .google_oauth_api import Credentials -from .storage_adapter import get_storage_adapter +from src.google_oauth_api import Credentials +from src.storage_adapter import get_storage_adapter class CredentialManager: """ diff --git a/src/google_oauth_api.py b/src/google_oauth_api.py index af0b2404b..59a7a63f7 100644 --- a/src/google_oauth_api.py +++ b/src/google_oauth_api.py @@ -18,7 +18,7 @@ ) from log import log -from .httpx_client import get_async, post_async +from src.httpx_client import get_async, post_async class TokenError(Exception): From fa5a928a2260962ffa97d996ef7180f15effe044 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 11:38:50 +0000 Subject: [PATCH 087/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index c81703652..23ab44339 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=6bd843709ff907aa892e43333481d6c7ec0ea78e -short_hash=6bd8437 -message=Update anthropic2gemini.py -date=2026-01-12 16:23:18 +0800 +full_hash=8101d14e1b1851b80a79b3a900332bdf01a7d5ae +short_hash=8101d14 +message=路径 +date=2026-01-12 19:37:52 +0800 From 0cf601fba5a403fcf7ef77117f59ac6069371a31 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 19:52:06 +0800 Subject: [PATCH 088/211] =?UTF-8?q?=E8=A7=84=E8=8C=83=E5=8C=96=E5=91=BD?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/common.js | 8 ++++---- src/web_routes.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/front/common.js b/front/common.js index 0988f7d72..1454ada59 100644 --- a/front/common.js +++ b/front/common.js @@ -341,7 +341,7 @@ function createCredsManager(type) { // ===================================================================== function createUploadManager(type) { const modeParam = type === 'antigravity' ? 'mode=antigravity' : 'mode=geminicli'; - const endpoint = `./auth/upload?${modeParam}`; + const endpoint = `./creds/upload?${modeParam}`; return { type: type, @@ -1920,7 +1920,7 @@ function connectWebSocket() { } try { - const wsPath = new URL('./auth/logs/stream', window.location.href).href; + const wsPath = new URL('./logs/stream', window.location.href).href; const wsUrl = wsPath.replace(/^http/, 'ws'); document.getElementById('connectionStatusText').textContent = '连接中...'; @@ -1986,7 +1986,7 @@ function clearLogsDisplay() { async function downloadLogs() { try { - const response = await fetch('./auth/logs/download', { headers: getAuthHeaders() }); + const response = await fetch('./logs/download', { headers: getAuthHeaders() }); if (response.ok) { const contentDisposition = response.headers.get('Content-Disposition'); @@ -2016,7 +2016,7 @@ async function downloadLogs() { async function clearLogs() { try { - const response = await fetch('./auth/logs/clear', { + const response = await fetch('./logs/clear', { method: 'POST', headers: getAuthHeaders() }); diff --git a/src/web_routes.py b/src/web_routes.py index 271dd347a..7d119936d 100644 --- a/src/web_routes.py +++ b/src/web_routes.py @@ -920,13 +920,13 @@ async def deduplicate_credentials_by_email_common(mode: str = "geminicli") -> JS # ============================================================================= -@router.post("/auth/upload") +@router.post("/creds/upload") async def upload_credentials( files: List[UploadFile] = File(...), token: str = Depends(verify_panel_token), mode: str = "geminicli" ): - """批量上传认证文件""" + """批量上传凭证文件""" try: mode = validate_mode(mode) return await upload_credentials_common(files, mode=mode) @@ -1497,7 +1497,7 @@ async def save_config(request: ConfigSaveRequest, token: str = Depends(verify_pa # ============================================================================= -@router.post("/auth/logs/clear") +@router.post("/logs/clear") async def clear_logs(token: str = Depends(verify_panel_token)): """清空日志文件""" try: @@ -1530,7 +1530,7 @@ async def clear_logs(token: str = Depends(verify_panel_token)): raise HTTPException(status_code=500, detail=f"清空日志文件失败: {str(e)}") -@router.get("/auth/logs/download") +@router.get("/logs/download") async def download_logs(token: str = Depends(verify_panel_token)): """下载日志文件""" try: @@ -1566,7 +1566,7 @@ async def download_logs(token: str = Depends(verify_panel_token)): raise HTTPException(status_code=500, detail=f"下载日志文件失败: {str(e)}") -@router.websocket("/auth/logs/stream") +@router.websocket("/logs/stream") async def websocket_logs(websocket: WebSocket): """WebSocket端点,用于实时日志流""" # 检查连接数限制 From 2e50b8e9f486cc100af5ffe291aee17777ae133a Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 19:57:35 +0800 Subject: [PATCH 089/211] Update README.md --- README.md | 59 +++++++++++++++++++++---------------------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index d0b4ac131..32e381fff 100644 --- a/README.md +++ b/README.md @@ -833,51 +833,36 @@ for part in response.candidates[0].content.parts: **认证端点** - `POST /auth/login` - 用户登录 -- `POST /auth/start` - 开始 GCLI OAuth 认证 -- `POST /auth/antigravity/start` - 开始 Antigravity OAuth 认证 +- `POST /auth/start` - 开始 OAuth 认证(支持 GCLI 和 Antigravity 模式) - `POST /auth/callback` - 处理 OAuth 回调 +- `POST /auth/callback-url` - 从回调 URL 直接完成认证 - `GET /auth/status/{project_id}` - 检查认证状态 -- `GET /auth/antigravity/credentials` - 获取 Antigravity 凭证 - -**GCLI 凭证管理端点** -- `GET /creds/status` - 获取所有 GCLI 凭证状态 -- `POST /creds/action` - 单个 GCLI 凭证操作(启用/禁用/删除) -- `POST /creds/batch-action` - 批量 GCLI 凭证操作 -- `POST /auth/upload` - 批量上传 GCLI 凭证文件(支持 ZIP) -- `GET /creds/download/{filename}` - 下载 GCLI 凭证文件 -- `GET /creds/download-all` - 打包下载所有 GCLI 凭证 -- `POST /creds/fetch-email/{filename}` - 获取 GCLI 用户邮箱 -- `POST /creds/refresh-all-emails` - 批量刷新 GCLI 用户邮箱 - -**Antigravity 凭证管理端点** -- `GET /antigravity/creds/status` - 获取所有 Antigravity 凭证状态 -- `POST /antigravity/creds/action` - 单个 Antigravity 凭证操作(启用/禁用/删除) -- `POST /antigravity/creds/batch-action` - 批量 Antigravity 凭证操作 -- `POST /antigravity/auth/upload` - 批量上传 Antigravity 凭证文件(支持 ZIP) -- `GET /antigravity/creds/download/{filename}` - 下载 Antigravity 凭证文件 -- `GET /antigravity/creds/download-all` - 打包下载所有 Antigravity 凭证 -- `POST /antigravity/creds/fetch-email/{filename}` - 获取 Antigravity 用户邮箱 -- `POST /antigravity/creds/refresh-all-emails` - 批量刷新 Antigravity 用户邮箱 + +**凭证管理端点**(支持 `mode=geminicli` 或 `mode=antigravity` 参数) +- `POST /creds/upload` - 批量上传凭证文件(支持 JSON 和 ZIP) +- `GET /creds/status` - 获取凭证状态列表(支持分页和筛选) +- `GET /creds/detail/{filename}` - 获取单个凭证详情 +- `POST /creds/action` - 单个凭证操作(启用/禁用/删除) +- `POST /creds/batch-action` - 批量凭证操作 +- `GET /creds/download/{filename}` - 下载单个凭证文件 +- `GET /creds/download-all` - 打包下载所有凭证 +- `POST /creds/fetch-email/{filename}` - 获取用户邮箱 +- `POST /creds/refresh-all-emails` - 批量刷新用户邮箱 +- `POST /creds/deduplicate-by-email` - 按邮箱去重凭证 +- `POST /creds/verify-project/{filename}` - 检验凭证 Project ID +- `GET /creds/quota/{filename}` - 获取凭证额度信息(仅 Antigravity) **配置管理端点** - `GET /config/get` - 获取当前配置 - `POST /config/save` - 保存配置 -**环境变量凭证端点** -- `POST /auth/load-env-creds` - 加载环境变量凭证 -- `DELETE /auth/env-creds` - 清除环境变量凭证 -- `GET /auth/env-creds-status` - 获取环境变量凭证状态 - **日志管理端点** -- `POST /auth/logs/clear` - 清空日志 -- `GET /auth/logs/download` - 下载日志文件 -- `WebSocket /auth/logs/stream` - 实时日志流 - -**使用统计端点** -- `GET /usage/stats` - 获取使用统计 -- `GET /usage/aggregated` - 获取聚合统计 -- `POST /usage/update-limits` - 更新使用限制 -- `POST /usage/reset` - 重置使用统计 +- `POST /logs/clear` - 清空日志 +- `GET /logs/download` - 下载日志文件 +- `WebSocket /logs/stream` - 实时日志流 + +**版本信息端点** +- `GET /version/info` - 获取版本信息(可选 `check_update=true` 参数检查更新) ### 聊天 API 功能特性 From f54939d02099ae2698a5bdcb4b7e96d6549b07e5 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 20:04:02 +0800 Subject: [PATCH 090/211] =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=8A=A0=E4=B8=8A?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/common.js | 5 ++++- src/web_routes.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/front/common.js b/front/common.js index 1454ada59..b07b90313 100644 --- a/front/common.js +++ b/front/common.js @@ -1923,10 +1923,13 @@ function connectWebSocket() { const wsPath = new URL('./logs/stream', window.location.href).href; const wsUrl = wsPath.replace(/^http/, 'ws'); + // 添加 token 认证参数 + const wsUrlWithAuth = `${wsUrl}?token=${encodeURIComponent(AppState.authToken)}`; + document.getElementById('connectionStatusText').textContent = '连接中...'; document.getElementById('logConnectionStatus').className = 'status info'; - AppState.logWebSocket = new WebSocket(wsUrl); + AppState.logWebSocket = new WebSocket(wsUrlWithAuth); AppState.logWebSocket.onopen = () => { document.getElementById('connectionStatusText').textContent = '已连接'; diff --git a/src/web_routes.py b/src/web_routes.py index 7d119936d..a256ca33e 100644 --- a/src/web_routes.py +++ b/src/web_routes.py @@ -1569,6 +1569,26 @@ async def download_logs(token: str = Depends(verify_panel_token)): @router.websocket("/logs/stream") async def websocket_logs(websocket: WebSocket): """WebSocket端点,用于实时日志流""" + # WebSocket 认证: 从查询参数获取 token + token = websocket.query_params.get("token") + + if not token: + await websocket.close(code=403, reason="Missing authentication token") + log.warning("WebSocket连接被拒绝: 缺少认证token") + return + + # 验证 token + try: + panel_password = await config.get_panel_password() + if token != panel_password: + await websocket.close(code=403, reason="Invalid authentication token") + log.warning("WebSocket连接被拒绝: token验证失败") + return + except Exception as e: + await websocket.close(code=1011, reason="Authentication error") + log.error(f"WebSocket认证过程出错: {e}") + return + # 检查连接数限制 if not await manager.connect(websocket): return From e4cc801950ef1b2c6a60310a3d8fe5520f658327 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 20:32:02 +0800 Subject: [PATCH 091/211] Update anthropic.py --- src/router/geminicli/anthropic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/router/geminicli/anthropic.py b/src/router/geminicli/anthropic.py index 14dd5c4b8..33f3391cf 100644 --- a/src/router/geminicli/anthropic.py +++ b/src/router/geminicli/anthropic.py @@ -20,7 +20,7 @@ from fastapi.responses import JSONResponse, StreamingResponse # 本地模块 - 配置和日志 -from config import get_anti_truncation_max_attempts, get_api_password +from config import get_anti_truncation_max_attempts from log import log # 本地模块 - 工具和认证 From 04942c32dea819cb7a4e0d1e7ab9203566da5cba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 12:32:20 +0000 Subject: [PATCH 092/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 23ab44339..8d309aefe 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=8101d14e1b1851b80a79b3a900332bdf01a7d5ae -short_hash=8101d14 -message=路径 -date=2026-01-12 19:37:52 +0800 +full_hash=e4cc801950ef1b2c6a60310a3d8fe5520f658327 +short_hash=e4cc801 +message=Update anthropic.py +date=2026-01-12 20:32:02 +0800 From 0fc95406fd68472578c7e13deecc5d02c0709f23 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 20:58:44 +0800 Subject: [PATCH 093/211] =?UTF-8?q?=E7=AE=80=E5=8C=96=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 8 ++++---- src/api/geminicli.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 77007c003..57ef6f9bc 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -237,7 +237,7 @@ async def refresh_credential(): credential_manager, current_file, mode="antigravity", model_key=model_name ) success_recorded = True - log.info(f"[ANTIGRAVITY STREAM] 开始接收流式响应,模型: {model_name}") + log.debug(f"[ANTIGRAVITY STREAM] 开始接收流式响应,模型: {model_name}") # 记录原始chunk内容(用于调试) if isinstance(chunk, bytes): @@ -249,7 +249,7 @@ async def refresh_credential(): # 流式请求完成,检查结果 if success_recorded: - log.info(f"[ANTIGRAVITY STREAM] 流式响应完成,模型: {model_name}") + log.debug(f"[ANTIGRAVITY STREAM] 流式响应完成,模型: {model_name}") return elif not need_retry: # 没有收到任何数据(空回复),需要重试 @@ -313,7 +313,7 @@ async def non_stream_request( """ # 检查是否启用流式收集模式 if await get_antigravity_stream2nostream(): - log.info("[ANTIGRAVITY] 使用流式收集模式实现非流式请求") + log.debug("[ANTIGRAVITY] 使用流式收集模式实现非流式请求") # 调用stream_request获取流 stream = stream_request(body=body, native=False, headers=headers) @@ -324,7 +324,7 @@ async def non_stream_request( return await collect_streaming_response(stream) # 否则使用传统非流式模式 - log.info("[ANTIGRAVITY] 使用传统非流式模式") + log.debug("[ANTIGRAVITY] 使用传统非流式模式") model_name = body.get("model", "") diff --git a/src/api/geminicli.py b/src/api/geminicli.py index 533d8fac2..c99a319e5 100644 --- a/src/api/geminicli.py +++ b/src/api/geminicli.py @@ -245,13 +245,13 @@ async def refresh_credential(): credential_manager, current_file, mode="geminicli", model_key=model_group ) success_recorded = True - log.info(f"[GEMINICLI STREAM] 开始接收流式响应,模型: {model_name}") + log.debug(f"[GEMINICLI STREAM] 开始接收流式响应,模型: {model_name}") yield chunk # 流式请求完成,检查结果 if success_recorded: - log.info(f"[GEMINICLI STREAM] 流式响应完成,模型: {model_name}") + log.debug(f"[GEMINICLI STREAM] 流式响应完成,模型: {model_name}") return elif not need_retry: # 没有收到任何数据(空回复),需要重试 From b01e36dabd0348bcbf3df383f34d52b0fbfeafc6 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 21:47:17 +0800 Subject: [PATCH 094/211] =?UTF-8?q?=E6=B7=BB=E5=8A=A0issue=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.yml | 92 +++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++ 2 files changed, 100 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..61f97efc9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,92 @@ +name: Bug 报告 +description: 报告项目使用中遇到的问题 +title: "[Bug]: " +labels: ["bug", "待处理"] +body: + - type: markdown + attributes: + value: | + ## 感谢你的反馈! + 请填写以下信息以帮助我们更快定位问题。 + + - type: checkboxes + id: checklist + attributes: + label: 提交前确认 + options: + - label: 我已经搜索过现有的 issues,确认这不是重复问题 + required: true + - label: 我已经阅读过项目文档 + required: true + + - type: dropdown + id: latest-version + attributes: + label: 是否是最新版 + description: 请确认你使用的是否是最新版本 + options: + - 是,使用最新版 + - 否,使用旧版本 + validations: + required: true + + - type: input + id: channel + attributes: + label: 调用的是哪个渠道 + description: 例如 geminicli 或者 antigravity + placeholder: "例如: geminicli" + validations: + required: true + + - type: input + id: model + attributes: + label: 调用的是哪个模型 + description: 例如 gemini-2.5-flash + placeholder: "例如: gemini-2.5-flash" + validations: + required: true + + - type: dropdown + id: format + attributes: + label: 调用的是哪个格式 + description: 选择你使用的 API 格式 + options: + - gemini 格式 + - openai 格式 + - claude 格式 + - 其他格式 + validations: + required: true + + - type: textarea + id: error-content + attributes: + label: 具体报错内容 + description: 请粘贴完整的错误信息或截图 + placeholder: | + 请在这里粘贴完整的错误日志或堆栈信息 + render: shell + validations: + required: true + + - type: textarea + id: error-description + attributes: + label: 错误描述 + description: 详细描述问题的发生场景、预期行为和实际行为 + placeholder: | + 1. 我在做什么操作时遇到了这个问题 + 2. 我期望的结果是... + 3. 但实际上发生了... + validations: + required: true + + - type: textarea + id: additional-context + attributes: + label: 补充信息(可选) + description: 其他任何有助于解决问题的信息 + placeholder: 例如:操作系统、Python 版本、相关配置等 \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..ceaef7e89 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 使用问题讨论 + url: https://github.com/su-kaka/gcli2api/issues + about: 如果是使用方面的问题,请在 issues 中提问 + - name: 项目文档 + url: https://github.com/su-kaka/gcli2api + about: 查看完整文档和使用指南 \ No newline at end of file From 4c09c94f827c2f713f7a36252e9621198b05caad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 13:47:32 +0000 Subject: [PATCH 095/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 8d309aefe..876e282bc 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=e4cc801950ef1b2c6a60310a3d8fe5520f658327 -short_hash=e4cc801 -message=Update anthropic.py -date=2026-01-12 20:32:02 +0800 +full_hash=b01e36dabd0348bcbf3df383f34d52b0fbfeafc6 +short_hash=b01e36d +message=添加issue模板 +date=2026-01-12 21:47:17 +0800 From ef8d8c83a6e3cedf9ff6f31e4d6d39cb6b07065f Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 21:53:40 +0800 Subject: [PATCH 096/211] Update openai2gemini.py --- src/converter/openai2gemini.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index c8a6222fa..9344a81fd 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -782,8 +782,8 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di if msg.get("role") == "assistant" and msg.get("tool_calls"): for tc in msg["tool_calls"]: encoded_id = tc.get("id", "") - func_name = tc.get("function", {}).get("name") - if encoded_id and func_name: + func_name = tc.get("function", {}).get("name") or "" + if encoded_id: # 解码获取原始ID和签名 original_id, signature = decode_tool_id_and_signature(encoded_id) tool_call_mapping[encoded_id] = (func_name, original_id, signature) @@ -822,12 +822,14 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di if func_name: break - if not func_name: - func_name = "unknown_function" - # 解码 tool_call_id 获取原始 ID original_id, _ = decode_tool_id_and_signature(tool_call_id) + # 最终兜底:确保 func_name 不为空 + if not func_name: + func_name = "unknown_function" + log.warning(f"Tool message missing function name for tool_call_id={tool_call_id}, using default: {func_name}") + # 解析响应数据 try: response_data = json.loads(content) if isinstance(content, str) else content From 9058f761d26d4062cc00e38073a54eab1bd5720a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 14:36:48 +0000 Subject: [PATCH 097/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 876e282bc..ad4a3693e 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=b01e36dabd0348bcbf3df383f34d52b0fbfeafc6 -short_hash=b01e36d -message=添加issue模板 -date=2026-01-12 21:47:17 +0800 +full_hash=c84645b1fe8d1bfbc134ad43b5a9e4a46e46feaa +short_hash=c84645b +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-12 22:30:25 +0800 From b70e3ccf1ca90763667b4616a00fbc19a36e94d4 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Mon, 12 Jan 2026 22:50:31 +0800 Subject: [PATCH 098/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index ea0f78794..c340ab83c 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional from log import log +from src.utils import DEFAULT_SAFETY_SETTINGS # ==================== Gemini API 配置 ==================== @@ -309,6 +310,9 @@ async def normalize_gemini_request( # ========== 公共处理 ========== + # 1. 安全设置覆盖 + result["safetySettings"] = DEFAULT_SAFETY_SETTINGS + # 2. 参数范围限制 if generation_config: max_tokens = generation_config.get("maxOutputTokens") From 54846e76ab23474997828b5dc54c41caebf3484f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 14:50:42 +0000 Subject: [PATCH 099/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index ad4a3693e..e88f588ad 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=c84645b1fe8d1bfbc134ad43b5a9e4a46e46feaa -short_hash=c84645b -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-12 22:30:25 +0800 +full_hash=b70e3ccf1ca90763667b4616a00fbc19a36e94d4 +short_hash=b70e3cc +message=Update gemini_fix.py +date=2026-01-12 22:50:31 +0800 From 6413fc6cb3e412f20879ec4db981b4cd89b410cc Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 13 Jan 2026 00:16:11 +0800 Subject: [PATCH 100/211] Update httpx_client.py --- src/httpx_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/httpx_client.py b/src/httpx_client.py index 45e29e26d..9f0db35b2 100644 --- a/src/httpx_client.py +++ b/src/httpx_client.py @@ -74,7 +74,7 @@ async def post_async( data: Any = None, json: Any = None, headers: Optional[Dict[str, str]] = None, - timeout: float = 30.0, + timeout: float = 600.0, **kwargs, ) -> httpx.Response: """通用异步POST请求""" From a5971320882199b7a83687a8913f5902dcf4db2d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 12 Jan 2026 16:16:22 +0000 Subject: [PATCH 101/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index e88f588ad..ad8081b33 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=b70e3ccf1ca90763667b4616a00fbc19a36e94d4 -short_hash=b70e3cc -message=Update gemini_fix.py -date=2026-01-12 22:50:31 +0800 +full_hash=6413fc6cb3e412f20879ec4db981b4cd89b410cc +short_hash=6413fc6 +message=Update httpx_client.py +date=2026-01-13 00:16:11 +0800 From 8870324296b0985325f08a8b28325beda1903dcc Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 13 Jan 2026 18:05:43 +0800 Subject: [PATCH 102/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index c340ab83c..21b24469d 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -197,15 +197,18 @@ async def normalize_gemini_request( # 假如 is_thinking_model 为真或者思考预算不为0,设置 thinkingConfig if is_thinking_model(model) or (thinking_budget and thinking_budget != 0): + # 确保 thinkingConfig 存在 if "thinkingConfig" not in generation_config: generation_config["thinkingConfig"] = {} - + + thinking_config = generation_config["thinkingConfig"] + # 设置思考预算 if thinking_budget: - generation_config["thinkingConfig"]["thinkingBudget"] = thinking_budget - + thinking_config["thinkingBudget"] = thinking_budget + # includeThoughts 使用配置值 - generation_config["thinkingConfig"]["includeThoughts"] = return_thoughts + thinking_config["includeThoughts"] = return_thoughts # 2. 搜索模型添加 Google Search if is_search_model(model): From 5e8782441cd6be66d0b8e51d847fa4dfc2ed14f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Jan 2026 10:05:53 +0000 Subject: [PATCH 103/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index ad8081b33..dd998deb1 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=6413fc6cb3e412f20879ec4db981b4cd89b410cc -short_hash=6413fc6 -message=Update httpx_client.py -date=2026-01-13 00:16:11 +0800 +full_hash=8870324296b0985325f08a8b28325beda1903dcc +short_hash=8870324 +message=Update gemini_fix.py +date=2026-01-13 18:05:43 +0800 From c25a637f19801264e2e6d2e7bc994b729b11931e Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 13 Jan 2026 18:29:36 +0800 Subject: [PATCH 104/211] =?UTF-8?q?=E8=AE=A4=E8=AF=81=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 9 +++++--- src/utils.py | 48 ++++++++++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 57ef6f9bc..17b0d67f6 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -60,11 +60,14 @@ def build_antigravity_headers(access_token: str, model_name: str = "") -> Dict[s # 根据模型名称判断 request_type if model_name: + # 先判断是否是图片模型 if "image" in model_name.lower(): - request_type = "image_gen" - else: + request_type = "image_gen" + headers['requestType'] = request_type + # 再判断是否不是claude模型 + if "claude" not in model_name.lower(): request_type = "agent" - headers['requestType'] = request_type + headers['requestType'] = request_type return headers diff --git a/src/utils.py b/src/utils.py index 5d3d2d6f8..1ec01f926 100644 --- a/src/utils.py +++ b/src/utils.py @@ -143,34 +143,40 @@ async def authenticate_flexible( x_api_key: Optional[str] = Header(None, alias="x-api-key"), access_token: Optional[str] = Header(None, alias="access_token"), x_goog_api_key: Optional[str] = Header(None, alias="x-goog-api-key"), + x_anthropic_auth_token: Optional[str] = Header(None, alias="x-anthropic-auth-token"), + anthropic_auth_token: Optional[str] = Header(None, alias="anthropic-auth-token"), key: Optional[str] = Query(None) ) -> str: """ 统一的灵活认证函数,支持多种认证方式 - + 此函数可以直接用作 FastAPI 的 Depends 依赖 - + 支持的认证方式: - URL 参数: key - HTTP 头部: Authorization (Bearer token) - HTTP 头部: x-api-key - HTTP 头部: access_token - HTTP 头部: x-goog-api-key - + - HTTP 头部: x-anthropic-auth-token + - HTTP 头部: anthropic-auth-token + Args: request: FastAPI Request 对象 authorization: Authorization 头部值(自动注入) x_api_key: x-api-key 头部值(自动注入) access_token: access_token 头部值(自动注入) x_goog_api_key: x-goog-api-key 头部值(自动注入) + x_anthropic_auth_token: x-anthropic-auth-token 头部值(自动注入) + anthropic_auth_token: anthropic-auth-token 头部值(自动注入) key: URL 参数 key(自动注入) - + Returns: 验证通过的token - + Raises: HTTPException: 认证失败时抛出异常 - + 使用示例: @router.post("/endpoint") async def endpoint(token: str = Depends(authenticate_flexible)): @@ -180,28 +186,38 @@ async def endpoint(token: str = Depends(authenticate_flexible)): password = await get_api_password() token = None auth_method = None - + # 1. 尝试从 URL 参数 key 获取(Google 官方标准方式) if key: token = key auth_method = "URL parameter 'key'" - + # 2. 尝试从 x-goog-api-key 头部获取(Google API 标准方式) elif x_goog_api_key: token = x_goog_api_key auth_method = "x-goog-api-key header" - - # 3. 尝试从 x-api-key 头部获取 + + # 3. 尝试从 x-anthropic-auth-token 头部获取(Anthropic 标准方式) + elif x_anthropic_auth_token: + token = x_anthropic_auth_token + auth_method = "x-anthropic-auth-token header" + + # 4. 尝试从 anthropic-auth-token 头部获取(Anthropic 替代方式) + elif anthropic_auth_token: + token = anthropic_auth_token + auth_method = "anthropic-auth-token header" + + # 5. 尝试从 x-api-key 头部获取 elif x_api_key: token = x_api_key auth_method = "x-api-key header" - - # 4. 尝试从 access_token 头部获取 + + # 6. 尝试从 access_token 头部获取 elif access_token: token = access_token auth_method = "access_token header" - - # 5. 尝试从 Authorization 头部获取 + + # 7. 尝试从 Authorization 头部获取 elif authorization: if not authorization.startswith("Bearer "): raise HTTPException( @@ -211,12 +227,12 @@ async def endpoint(token: str = Depends(authenticate_flexible)): ) token = authorization[7:] # 移除 "Bearer " 前缀 auth_method = "Authorization Bearer header" - + # 检查是否提供了任何认证凭据 if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Missing authentication credentials. Use 'key' URL parameter, 'x-goog-api-key', 'x-api-key', 'access_token' header, or 'Authorization: Bearer '", + detail="Missing authentication credentials. Use 'key' URL parameter, 'x-goog-api-key', 'x-anthropic-auth-token', 'anthropic-auth-token', 'x-api-key', 'access_token' header, or 'Authorization: Bearer '", headers={"WWW-Authenticate": "Bearer"}, ) From e37ba02b9c335bb6de285e97f36bd638ff39cb47 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Jan 2026 10:29:45 +0000 Subject: [PATCH 105/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index dd998deb1..986d0e8a4 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=8870324296b0985325f08a8b28325beda1903dcc -short_hash=8870324 -message=Update gemini_fix.py -date=2026-01-13 18:05:43 +0800 +full_hash=c25a637f19801264e2e6d2e7bc994b729b11931e +short_hash=c25a637 +message=认证支持更多字段 +date=2026-01-13 18:29:36 +0800 From 40b9b1e034bdcd0910a7235f3eb24906e9fd4bf9 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 13 Jan 2026 20:42:21 +0800 Subject: [PATCH 106/211] Update antigravity.py --- src/api/antigravity.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 17b0d67f6..bc7614c2f 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -64,8 +64,7 @@ def build_antigravity_headers(access_token: str, model_name: str = "") -> Dict[s if "image" in model_name.lower(): request_type = "image_gen" headers['requestType'] = request_type - # 再判断是否不是claude模型 - if "claude" not in model_name.lower(): + else: request_type = "agent" headers['requestType'] = request_type From fb33771cab955556a90b4fd7b349f2142cdf14cd Mon Sep 17 00:00:00 2001 From: sycghj Date: Tue, 13 Jan 2026 22:16:33 +0800 Subject: [PATCH 107/211] fix: use actual token counts in message_start event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 message_start 事件中 usage 硬编码为 0 的问题 问题: - message_start 的 usage 被硬编码为 {input_tokens: 0, output_tokens: 0} - 即使已从 usageMetadata 更新了变量值,仍使用硬编码的 0 - 不符合 Anthropic API 规范,影响客户端 token 统计显示 修复: - 使用实际的 input_tokens 和 output_tokens 变量值 - 同时修复异常处理路径中的相同问题 影响: - 提高与 Anthropic Streaming API 的兼容性 - 改善客户端 token 使用可见性 --- src/converter/anthropic2gemini.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index a69525c68..9a196288d 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -1017,7 +1017,7 @@ def _close_block() -> Optional[bytes]: "content": [], "stop_reason": None, "stop_sequence": None, - "usage": {"input_tokens": 0, "output_tokens": 0}, + "usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}, }, }, ) @@ -1239,7 +1239,7 @@ def _close_block() -> Optional[bytes]: "content": [], "stop_reason": None, "stop_sequence": None, - "usage": {"input_tokens": 0, "output_tokens": 0}, + "usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}, }, }, ) From 0f3dc6e2acce07d6f946db659ffe8e8840b1d349 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 00:04:13 +0800 Subject: [PATCH 108/211] py313 --- .python-version | 2 +- pyproject.toml | 7 +++---- runtime.txt | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 runtime.txt diff --git a/.python-version b/.python-version index e4fba2183..24ee5b1be 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12 +3.13 diff --git a/pyproject.toml b/pyproject.toml index 403e7028a..d437da2a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "gcli2api" version = "0.1.0" description = "Convert GeminiCLI to OpenAI and Gemini API interfaces" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.13" license = {text = "CNC-1.0"} authors = [ {name = "su-kaka"} @@ -14,7 +14,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: Other/Proprietary License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] dependencies = [ @@ -57,7 +56,7 @@ addopts = [ [tool.black] line-length = 100 -target-version = ["py312"] +target-version = ["py313"] include = '\.pyi?$' extend-exclude = ''' /( @@ -74,7 +73,7 @@ extend-exclude = ''' ''' [tool.mypy] -python_version = "3.12" +python_version = "3.13" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = false diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index 0e1ecfb21..000000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.12.7 \ No newline at end of file From d6f698316f8876b6f15dc33201caca0386c183b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Jan 2026 16:04:25 +0000 Subject: [PATCH 109/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 986d0e8a4..53a78ffad 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=c25a637f19801264e2e6d2e7bc994b729b11931e -short_hash=c25a637 -message=认证支持更多字段 -date=2026-01-13 18:29:36 +0800 +full_hash=0f3dc6e2acce07d6f946db659ffe8e8840b1d349 +short_hash=0f3dc6e +message=py313 +date=2026-01-14 00:04:13 +0800 From 472f00f8592c4add1c2abe1038bfea885acc9f78 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 00:16:45 +0800 Subject: [PATCH 110/211] Update openai2gemini.py --- src/converter/openai2gemini.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 9344a81fd..66c7e8539 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -586,6 +586,10 @@ def convert_tool_message_to_function_response(message, all_messages: List = None # 如果不是有效的 JSON,包装为对象 response_data = {"result": str(message.content)} + # 确保 response_data 是字典类型(Gemini API 要求 response 必须是对象) + if not isinstance(response_data, dict): + response_data = {"result": response_data} + return {"functionResponse": {"id": original_tool_call_id, "name": name, "response": response_data}} @@ -836,6 +840,10 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di except (json.JSONDecodeError, TypeError): response_data = {"result": str(content)} + # 确保 response_data 是字典类型(Gemini API 要求 response 必须是对象) + if not isinstance(response_data, dict): + response_data = {"result": response_data} + # 使用原始 ID(不带签名) contents.append({ "role": "user", From 91644588f4c958099f4dd56b926101852c229700 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Jan 2026 16:17:40 +0000 Subject: [PATCH 111/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 53a78ffad..b9f35eb44 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=0f3dc6e2acce07d6f946db659ffe8e8840b1d349 -short_hash=0f3dc6e -message=py313 -date=2026-01-14 00:04:13 +0800 +full_hash=e541f8434f388b6e54468bdcde25fda0eb092c8f +short_hash=e541f84 +message=Merge pull request #276 from sycghj/fix/message-start-usage-hardcoded +date=2026-01-14 00:17:30 +0800 From 494f2af665fc14ccf4f0902ed63e452ca6f0094d Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 01:19:50 +0800 Subject: [PATCH 112/211] Update termux-install.sh --- termux-install.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/termux-install.sh b/termux-install.sh index 4a9d8af54..e6c5ffb67 100644 --- a/termux-install.sh +++ b/termux-install.sh @@ -51,7 +51,6 @@ ensure_dpkg_ready() { dpkg --configure -a || true } - # 更新包列表并检查错误 echo "正在更新包列表..." ensure_dpkg_ready @@ -146,15 +145,29 @@ echo "强制同步项目代码,忽略本地修改..." git fetch --all git reset --hard origin/$(git rev-parse --abbrev-ref HEAD) +uv python pin 3.12 + +# 验证 .python-version 文件 +if [ -f ".python-version" ]; then + echo "✅ Python 版本已固定到: $(cat .python-version)" +else + echo "⚠️ 警告: .python-version 文件未创建" +fi + echo "初始化 uv 环境..." uv init +echo "创建虚拟环境..." +uv venv + echo "安装 Python 依赖..." -uv add -r requirements-termux.txt +uv pip install -r requirements-termux.txt echo "激活虚拟环境并启动服务..." source .venv/bin/activate pm2 start .venv/bin/python --name web -- web.py cd .. -echo "✅ 安装完成!服务已启动。" \ No newline at end of file +echo "✅ 安装完成!服务已启动。" +echo "📌 Python 版本已固定为: $PYTHON_VERSION" +echo "📄 查看固定版本: cat gcli2api/.python-version" \ No newline at end of file From f3d597952a41f661d50fd4e16fcf95270be5a228 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Jan 2026 17:20:01 +0000 Subject: [PATCH 113/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index b9f35eb44..1a1b9fe5f 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=e541f8434f388b6e54468bdcde25fda0eb092c8f -short_hash=e541f84 -message=Merge pull request #276 from sycghj/fix/message-start-usage-hardcoded -date=2026-01-14 00:17:30 +0800 +full_hash=494f2af665fc14ccf4f0902ed63e452ca6f0094d +short_hash=494f2af +message=Update termux-install.sh +date=2026-01-14 01:19:50 +0800 From 963aa6ce94e86069468f0aea8e16b238fdb38d33 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 01:24:07 +0800 Subject: [PATCH 114/211] =?UTF-8?q?=E8=A7=A3=E5=86=B3termux=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .python-version | 1 - termux-install.sh | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 .python-version diff --git a/.gitignore b/.gitignore index d1a9d3b92..c1fab8d85 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ GEMINI.md # Python uv.lock +.python-version __pycache__/ *.py[cod] *$py.class diff --git a/.python-version b/.python-version deleted file mode 100644 index 24ee5b1be..000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.13 diff --git a/termux-install.sh b/termux-install.sh index e6c5ffb67..b30439bea 100644 --- a/termux-install.sh +++ b/termux-install.sh @@ -155,6 +155,7 @@ else fi echo "初始化 uv 环境..." +rm pyproject.toml uv init echo "创建虚拟环境..." From d73378a10a29b2529ba396e9cef7fce4dafd6ef9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Jan 2026 17:24:18 +0000 Subject: [PATCH 115/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 1a1b9fe5f..35a48284e 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=494f2af665fc14ccf4f0902ed63e452ca6f0094d -short_hash=494f2af -message=Update termux-install.sh -date=2026-01-14 01:19:50 +0800 +full_hash=963aa6ce94e86069468f0aea8e16b238fdb38d33 +short_hash=963aa6c +message=解决termux安装问题 +date=2026-01-14 01:24:07 +0800 From 8a3a1be0f233489c712416a503fca05e1a4b6e47 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 01:28:17 +0800 Subject: [PATCH 116/211] Update termux-install.sh --- termux-install.sh | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/termux-install.sh b/termux-install.sh index b30439bea..37628ffbc 100644 --- a/termux-install.sh +++ b/termux-install.sh @@ -147,13 +147,6 @@ git reset --hard origin/$(git rev-parse --abbrev-ref HEAD) uv python pin 3.12 -# 验证 .python-version 文件 -if [ -f ".python-version" ]; then - echo "✅ Python 版本已固定到: $(cat .python-version)" -else - echo "⚠️ 警告: .python-version 文件未创建" -fi - echo "初始化 uv 环境..." rm pyproject.toml uv init @@ -167,8 +160,4 @@ uv pip install -r requirements-termux.txt echo "激活虚拟环境并启动服务..." source .venv/bin/activate pm2 start .venv/bin/python --name web -- web.py -cd .. - -echo "✅ 安装完成!服务已启动。" -echo "📌 Python 版本已固定为: $PYTHON_VERSION" -echo "📄 查看固定版本: cat gcli2api/.python-version" \ No newline at end of file +cd .. \ No newline at end of file From 5b856575a442d4de322e31f9fce39333bcad587d Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 01:36:10 +0800 Subject: [PATCH 117/211] Update termux-install.sh --- termux-install.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/termux-install.sh b/termux-install.sh index 37628ffbc..2dc8e4046 100644 --- a/termux-install.sh +++ b/termux-install.sh @@ -145,17 +145,19 @@ echo "强制同步项目代码,忽略本地修改..." git fetch --all git reset --hard origin/$(git rev-parse --abbrev-ref HEAD) -uv python pin 3.12 - -echo "初始化 uv 环境..." -rm pyproject.toml -uv init - -echo "创建虚拟环境..." -uv venv +# 只在不存在时创建 +if [ ! -d ".venv" ]; then + echo "创建虚拟环境..." + rm pyproject.toml + uv python pin 3.12 + uv init + uv venv +else + echo "虚拟环境已存在,跳过创建" +fi echo "安装 Python 依赖..." -uv pip install -r requirements-termux.txt +uv add -r requirements-termux.txt echo "激活虚拟环境并启动服务..." source .venv/bin/activate From aa09ed6ea7fdb83c1edc60b4fa3927915f58b4df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Jan 2026 17:37:06 +0000 Subject: [PATCH 118/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 35a48284e..56a6536af 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=963aa6ce94e86069468f0aea8e16b238fdb38d33 -short_hash=963aa6c -message=解决termux安装问题 -date=2026-01-14 01:24:07 +0800 +full_hash=8d9bd1247f763af81215acf9dddd721bc6c514f0 +short_hash=8d9bd12 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-14 01:36:23 +0800 From 81ece786898f3fa9c603b00626c29e9d6a2fb4cf Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 01:38:41 +0800 Subject: [PATCH 119/211] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d437da2a7..7693d8cc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "gcli2api" version = "0.1.0" description = "Convert GeminiCLI to OpenAI and Gemini API interfaces" readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.12" license = {text = "CNC-1.0"} authors = [ {name = "su-kaka"} From e6054d514a5292794f5d846cb9e1ba1eca869570 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 Jan 2026 17:38:50 +0000 Subject: [PATCH 120/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 56a6536af..b70d6779d 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=8d9bd1247f763af81215acf9dddd721bc6c514f0 -short_hash=8d9bd12 -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-14 01:36:23 +0800 +full_hash=81ece786898f3fa9c603b00626c29e9d6a2fb4cf +short_hash=81ece78 +message=Update pyproject.toml +date=2026-01-14 01:38:41 +0800 From 897efa8e165f923499fd759d1e7a76f28ce42404 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 12:02:51 +0800 Subject: [PATCH 121/211] Update openai2gemini.py --- src/converter/openai2gemini.py | 177 +++++++++++++++++++++++++++++++-- 1 file changed, 168 insertions(+), 9 deletions(-) diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 66c7e8539..928539fe7 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -164,12 +164,161 @@ def _resolve_ref(ref: str, root_schema: Dict[str, Any]) -> Optional[Dict[str, An return current if isinstance(current, dict) else None +def _clean_schema_for_claude(schema: Any, root_schema: Optional[Dict[str, Any]] = None, visited: Optional[set] = None) -> Any: + """ + 清理 JSON Schema,转换为 Claude API 支持的格式(符合 JSON Schema draft 2020-12) + + 处理逻辑: + 1. 解析 $ref 引用 + 2. 合并 allOf 中的 schema + 3. 转换 anyOf 为更兼容的格式 + 4. 保持标准 JSON Schema 类型(不转换为大写) + 5. 处理 array 的 items + 6. 清理 Claude 不支持的字段 + + Args: + schema: JSON Schema 对象 + root_schema: 根 schema(用于解析 $ref) + visited: 已访问的对象集合(防止循环引用) + + Returns: + 清理后的 schema + """ + # 非字典类型直接返回 + if not isinstance(schema, dict): + return schema + + # 初始化 + if root_schema is None: + root_schema = schema + if visited is None: + visited = set() + + # 防止循环引用 + schema_id = id(schema) + if schema_id in visited: + return schema + visited.add(schema_id) + + # 创建副本避免修改原对象 + result = {} + + # 1. 处理 $ref + if "$ref" in schema: + resolved = _resolve_ref(schema["$ref"], root_schema) + if resolved: + import copy + result = copy.deepcopy(resolved) + for key, value in schema.items(): + if key != "$ref": + result[key] = value + schema = result + result = {} + + # 2. 处理 allOf(合并所有 schema) + if "allOf" in schema: + all_of_schemas = schema["allOf"] + for item in all_of_schemas: + cleaned_item = _clean_schema_for_claude(item, root_schema, visited) + + if "properties" in cleaned_item: + if "properties" not in result: + result["properties"] = {} + result["properties"].update(cleaned_item["properties"]) + + if "required" in cleaned_item: + if "required" not in result: + result["required"] = [] + result["required"].extend(cleaned_item["required"]) + + for key, value in cleaned_item.items(): + if key not in ["properties", "required"]: + result[key] = value + + for key, value in schema.items(): + if key not in ["allOf", "properties", "required"]: + result[key] = value + elif key in ["properties", "required"] and key not in result: + result[key] = value + else: + result = dict(schema) + + # 3. 处理 type 数组(如 ["string", "null"]) + if "type" in result: + type_value = result["type"] + if isinstance(type_value, list): + # Claude 支持 type 数组,保持不变 + pass + + # 4. 处理 array 的 items + if result.get("type") == "array": + if "items" not in result: + result["items"] = {} + elif isinstance(result["items"], list): + # Tuple 定义,检查是否所有元素类型相同 + tuple_items = result["items"] + first_type = tuple_items[0].get("type") if tuple_items else None + is_homogeneous = all(item.get("type") == first_type for item in tuple_items) + + if is_homogeneous and first_type: + result["items"] = _clean_schema_for_claude(tuple_items[0], root_schema, visited) + else: + # 异质元组,使用 anyOf 表示 + result["items"] = { + "anyOf": [_clean_schema_for_claude(item, root_schema, visited) for item in tuple_items] + } + else: + result["items"] = _clean_schema_for_claude(result["items"], root_schema, visited) + + # 5. 处理 anyOf(保持 anyOf,递归清理) + if "anyOf" in result: + result["anyOf"] = [_clean_schema_for_claude(item, root_schema, visited) for item in result["anyOf"]] + + # 6. 清理 Claude 不支持的字段(根据 JSON Schema 2020-12) + # Claude API 对某些字段比较严格,移除可能导致问题的字段 + unsupported_keys = { + "title", "$schema", "strict", + "additionalItems", # 废弃字段,使用 items 替代 + "exclusiveMaximum", "exclusiveMinimum", # 在 2020-12 中这些应该是数值而非布尔值 + "$defs", "definitions", # 移除 definitions 相关字段避免冲突 + "example", "examples", "readOnly", "writeOnly", + "const", # const 可能导致问题 + "contentEncoding", "contentMediaType", + "oneOf", # oneOf 可能导致问题,用 anyOf 替代 + } + + for key in list(result.keys()): + if key in unsupported_keys: + del result[key] + + # 递归处理 additionalProperties(如果存在) + if "additionalProperties" in result and isinstance(result["additionalProperties"], dict): + result["additionalProperties"] = _clean_schema_for_claude(result["additionalProperties"], root_schema, visited) + + # 7. 递归处理 properties + if "properties" in result: + cleaned_props = {} + for prop_name, prop_schema in result["properties"].items(): + cleaned_props[prop_name] = _clean_schema_for_claude(prop_schema, root_schema, visited) + result["properties"] = cleaned_props + + # 8. 确保有 type 字段(如果有 properties 但没有 type) + if "properties" in result and "type" not in result: + result["type"] = "object" + + # 9. 去重 required 数组 + if "required" in result and isinstance(result["required"], list): + result["required"] = list(dict.fromkeys(result["required"])) + + return result + + def _clean_schema_for_gemini(schema: Any, root_schema: Optional[Dict[str, Any]] = None, visited: Optional[set] = None) -> Any: """ 清理 JSON Schema,转换为 Gemini 支持的格式 - + 参考 worker.mjs 的 transformOpenApiSchemaToGemini 实现 - + 处理逻辑: 1. 解析 $ref 引用 2. 合并 allOf 中的 schema @@ -178,12 +327,12 @@ def _clean_schema_for_gemini(schema: Any, root_schema: Optional[Dict[str, Any]] 5. 处理 ARRAY 的 items(包括 Tuple) 6. 将 default 值移到 description 7. 清理不支持的字段 - + Args: schema: JSON Schema 对象 root_schema: 根 schema(用于解析 $ref) visited: 已访问的对象集合(防止循环引用) - + Returns: 清理后的 schema """ @@ -449,12 +598,13 @@ def fix_tool_call_args_types( return fixed_args -def convert_openai_tools_to_gemini(openai_tools: List) -> List[Dict[str, Any]]: +def convert_openai_tools_to_gemini(openai_tools: List, model: str = "") -> List[Dict[str, Any]]: """ 将 OpenAI tools 格式转换为 Gemini functionDeclarations 格式 Args: openai_tools: OpenAI 格式的工具列表(可能是字典或 Pydantic 模型) + model: 模型名称(用于判断是否为 Claude 模型) Returns: Gemini 格式的工具列表 @@ -462,6 +612,9 @@ def convert_openai_tools_to_gemini(openai_tools: List) -> List[Dict[str, Any]]: if not openai_tools: return [] + # 判断是否为 Claude 模型 + is_claude_model = "claude" in model.lower() + function_declarations = [] for tool in openai_tools: @@ -492,9 +645,14 @@ def convert_openai_tools_to_gemini(openai_tools: List) -> List[Dict[str, Any]]: "description": function.get("description", ""), } - # 添加参数(如果有)- 清理不支持的 schema 字段 + # 添加参数(如果有)- 根据模型选择不同的清理函数 if "parameters" in function: - cleaned_params = _clean_schema_for_gemini(function["parameters"]) + if is_claude_model: + cleaned_params = _clean_schema_for_claude(function["parameters"]) + log.debug(f"[OPENAI2GEMINI] Using Claude schema cleaning for tool: {normalized_name}") + else: + cleaned_params = _clean_schema_for_gemini(function["parameters"]) + if cleaned_params: declaration["parameters"] = cleaned_params @@ -1000,9 +1158,10 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di if "systemInstruction" in openai_request: gemini_request["systemInstruction"] = openai_request["systemInstruction"] - # 处理工具 + # 处理工具 - 传递 model 参数以便根据模型类型选择清理策略 + model = openai_request.get("model", "") if "tools" in openai_request and openai_request["tools"]: - gemini_request["tools"] = convert_openai_tools_to_gemini(openai_request["tools"]) + gemini_request["tools"] = convert_openai_tools_to_gemini(openai_request["tools"], model) # 处理tool_choice if "tool_choice" in openai_request and openai_request["tool_choice"]: From a7ceb13cf0e7f4c3feda0b3e3395fbcb98cad774 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 04:03:17 +0000 Subject: [PATCH 122/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index b70d6779d..9fdc4ce30 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=81ece786898f3fa9c603b00626c29e9d6a2fb4cf -short_hash=81ece78 -message=Update pyproject.toml -date=2026-01-14 01:38:41 +0800 +full_hash=98e762a704a9cca0f811cbede3828ff892c24140 +short_hash=98e762a +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-14 12:03:04 +0800 From ca3f07d0f673e6b4527224f04dfe59b74163774d Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 20:03:18 +0800 Subject: [PATCH 123/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 21b24469d..4aff9f4c7 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -228,7 +228,18 @@ async def normalize_gemini_request( existing_parts = [] if system_instruction: if isinstance(system_instruction, dict): - existing_parts = system_instruction.get("parts", []) + raw_parts = system_instruction.get("parts", []) + # 规范化 existing_parts: 确保每个 part 的 text 字段是字符串而不是数组 + for part in raw_parts: + if isinstance(part, dict): + text_value = part.get("text") + if isinstance(text_value, list): + # 如果 text 是数组, 将其展平为字符串 + part = part.copy() + part["text"] = " ".join(str(t) for t in text_value if t) + existing_parts.append(part) + else: + existing_parts.append(part) # custom_prompt 始终放在第一位,原有内容整体后移 result["systemInstruction"] = { From 8be565ce1eb8b29268b638cdb4c725be6e3a7464 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 12:08:35 +0000 Subject: [PATCH 124/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 9fdc4ce30..8b5ef6cd3 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=98e762a704a9cca0f811cbede3828ff892c24140 -short_hash=98e762a -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-14 12:03:04 +0800 +full_hash=ca3f07d0f673e6b4527224f04dfe59b74163774d +short_hash=ca3f07d +message=Update gemini_fix.py +date=2026-01-14 20:03:18 +0800 From abd4a3ee6fb43107f2a552c481342bb268b0bac4 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 20:09:23 +0800 Subject: [PATCH 125/211] Revert "Update gemini_fix.py" This reverts commit ca3f07d0f673e6b4527224f04dfe59b74163774d. --- src/converter/gemini_fix.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 4aff9f4c7..21b24469d 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -228,18 +228,7 @@ async def normalize_gemini_request( existing_parts = [] if system_instruction: if isinstance(system_instruction, dict): - raw_parts = system_instruction.get("parts", []) - # 规范化 existing_parts: 确保每个 part 的 text 字段是字符串而不是数组 - for part in raw_parts: - if isinstance(part, dict): - text_value = part.get("text") - if isinstance(text_value, list): - # 如果 text 是数组, 将其展平为字符串 - part = part.copy() - part["text"] = " ".join(str(t) for t in text_value if t) - existing_parts.append(part) - else: - existing_parts.append(part) + existing_parts = system_instruction.get("parts", []) # custom_prompt 始终放在第一位,原有内容整体后移 result["systemInstruction"] = { From d0975620e9617f4bde2eae1005e6b5b113bf3572 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 12:09:51 +0000 Subject: [PATCH 126/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 8b5ef6cd3..061e49bef 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=ca3f07d0f673e6b4527224f04dfe59b74163774d -short_hash=ca3f07d -message=Update gemini_fix.py -date=2026-01-14 20:03:18 +0800 +full_hash=38fa4f6d886b5f18c9e660fdfe4678612d41df25 +short_hash=38fa4f6 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-14 20:09:39 +0800 From 74d2163af1e00f1fab0aa43de1d1bbc3345776be Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 20:15:31 +0800 Subject: [PATCH 127/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 21b24469d..e3d591977 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -345,10 +345,23 @@ async def normalize_gemini_request( ) if has_valid_value: - # 清理 text 字段的尾随空格 - if "text" in part and isinstance(part["text"], str): - part = part.copy() - part["text"] = part["text"].rstrip() + part = part.copy() + + # 修复 text 字段:确保是字符串而不是列表 + if "text" in part: + text_value = part["text"] + if isinstance(text_value, list): + # 如果是列表,合并为字符串 + log.warning(f"[GEMINI_FIX] text 字段是列表,自动合并: {text_value}") + part["text"] = " ".join(str(t) for t in text_value if t) + elif isinstance(text_value, str): + # 清理尾随空格 + part["text"] = text_value.rstrip() + else: + # 其他类型转为字符串 + log.warning(f"[GEMINI_FIX] text 字段类型异常 ({type(text_value)}), 转为字符串: {text_value}") + part["text"] = str(text_value) + valid_parts.append(part) else: log.warning(f"[GEMINI_FIX] 移除空的或无效的 part: {part}") From c7555a3c51be1eb1c77ed4ed8908592b7c7009e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 12:24:38 +0000 Subject: [PATCH 128/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 061e49bef..81ec4ae7a 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=38fa4f6d886b5f18c9e660fdfe4678612d41df25 -short_hash=38fa4f6 +full_hash=f85b19e4ba1dc664cdc6b442d6e4a319225d1c90 +short_hash=f85b19e message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-14 20:09:39 +0800 +date=2026-01-14 20:24:26 +0800 From 553a5252ae03e08448fbf6c4363686f47c4faa4e Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 21:47:02 +0800 Subject: [PATCH 129/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index e3d591977..fdca65fd6 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -318,13 +318,10 @@ async def normalize_gemini_request( # 2. 参数范围限制 if generation_config: - max_tokens = generation_config.get("maxOutputTokens") - if max_tokens is not None: - generation_config["maxOutputTokens"] = 64000 - - top_k = generation_config.get("topK") - if top_k is not None: - generation_config["topK"] = 64 + # 强制设置 maxOutputTokens 为 64000 + generation_config["maxOutputTokens"] = 64000 + # 强制设置 topK 为 64 + generation_config["topK"] = 64 if "contents" in result: cleaned_contents = [] From 430756d3ba7fa107cc8a43acbe805d5c1242055e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 13:47:12 +0000 Subject: [PATCH 130/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 81ec4ae7a..22591fdea 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=f85b19e4ba1dc664cdc6b442d6e4a319225d1c90 -short_hash=f85b19e -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-14 20:24:26 +0800 +full_hash=553a5252ae03e08448fbf6c4363686f47c4faa4e +short_hash=553a525 +message=Update gemini_fix.py +date=2026-01-14 21:47:02 +0800 From 36d7ceef61a311837bb57fa66254abfca0bd1742 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 21:55:29 +0800 Subject: [PATCH 131/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index fdca65fd6..d9b16f862 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -311,6 +311,10 @@ async def normalize_gemini_request( if original_model != model: log.debug(f"[ANTIGRAVITY] 映射模型: {original_model} -> {model}") + # 5. 移除 antigravity 模式不支持的字段 + generation_config.pop("presencePenalty", None) + generation_config.pop("frequencyPenalty", None) + # ========== 公共处理 ========== # 1. 安全设置覆盖 From ff995a9a61479a7de5da0e387cd4c2d48da56ec3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 13:55:40 +0000 Subject: [PATCH 132/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 22591fdea..34a2292fe 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=553a5252ae03e08448fbf6c4363686f47c4faa4e -short_hash=553a525 +full_hash=36d7ceef61a311837bb57fa66254abfca0bd1742 +short_hash=36d7cee message=Update gemini_fix.py -date=2026-01-14 21:47:02 +0800 +date=2026-01-14 21:55:29 +0800 From 160294d9afdd47f12bc469b7dd8ab9feb92db866 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Wed, 14 Jan 2026 23:08:17 +0800 Subject: [PATCH 133/211] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 143 ++++++++++++++++++--------- src/api/geminicli.py | 215 +++++++++++++++++++++++++++++------------ 2 files changed, 250 insertions(+), 108 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index bc7614c2f..623692a93 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -143,9 +143,10 @@ async def stream_request( DISABLE_ERROR_CODES = await get_auto_ban_error_codes() # 禁用凭证的错误码 last_error_response = None # 记录最后一次的错误响应 - - # 内部函数:获取新凭证并更新headers - async def refresh_credential(): + next_cred_task = None # 预热的下一个凭证任务 + + # 内部函数:快速更新凭证(只更新token和project_id,避免重建整个请求) + async def refresh_credential_fast(): nonlocal current_file, access_token, auth_headers, project_id, final_payload cred_result = await credential_manager.get_valid_credential( mode="antigravity", model_key=model_name @@ -157,10 +158,9 @@ async def refresh_credential(): project_id = credential_data.get("project_id", "") if not access_token: return None - auth_headers = build_antigravity_headers(access_token, model_name) - if headers: - auth_headers.update(headers) - final_payload = {"model": body.get("model"), "project": project_id, "request": body.get("request", {})} + # 只更新token和project_id,不重建整个headers和payload + auth_headers["Authorization"] = f"Bearer {access_token}" + final_payload["project"] = project_id return True for attempt in range(max_retries + 1): @@ -179,21 +179,30 @@ async def refresh_credential(): status_code = chunk.status_code last_error_response = chunk # 记录最后一次错误 + # 缓存错误解析结果,避免重复decode + error_body = None + try: + error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) + except Exception: + error_body = "" + # 如果错误码是429或者在禁用码当中,做好记录后进行重试 if status_code == 429 or status_code in DISABLE_ERROR_CODES: - # 解析错误响应内容 - try: - error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) - log.warning(f"[ANTIGRAVITY STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") - except Exception: - log.warning(f"[ANTIGRAVITY STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}") + log.warning(f"[ANTIGRAVITY STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500] if error_body else '无'}") + + # 并行预热下一个凭证,不阻塞当前处理 + if next_cred_task is None and attempt < max_retries: + next_cred_task = asyncio.create_task( + credential_manager.get_valid_credential( + mode="antigravity", model_key=model_name + ) + ) # 记录错误 cooldown_until = None - if status_code == 429: - # 尝试解析冷却时间 + if status_code == 429 and error_body: + # 使用已缓存的error_body解析冷却时间 try: - error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) cooldown_until = await parse_and_log_cooldown(error_body, mode="antigravity") except Exception: pass @@ -220,11 +229,7 @@ async def refresh_credential(): return else: # 错误码不在禁用码当中,直接返回,无需重试 - try: - error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) - log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") - except Exception: - log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}") + log.error(f"[ANTIGRAVITY STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500] if error_body else '无'}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="antigravity", model_key=model_name @@ -275,9 +280,30 @@ async def refresh_credential(): # 统一处理重试 if need_retry: log.info(f"[ANTIGRAVITY STREAM] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") + + # 使用预热的凭证任务,避免等待 + if next_cred_task is not None: + try: + cred_result = await next_cred_task + next_cred_task = None # 重置任务 + + if cred_result: + current_file, credential_data = cred_result + access_token = credential_data.get("access_token") or credential_data.get("token") + project_id = credential_data.get("project_id", "") + if access_token and project_id: + auth_headers["Authorization"] = f"Bearer {access_token}" + final_payload["project"] = project_id + await asyncio.sleep(retry_interval) + continue # 重试 + except Exception as e: + log.warning(f"[ANTIGRAVITY STREAM] 预热凭证任务失败: {e}") + next_cred_task = None + + # 如果预热的凭证不可用,则同步获取 await asyncio.sleep(retry_interval) - - if not await refresh_credential(): + + if not await refresh_credential_fast(): log.error("[ANTIGRAVITY STREAM] 重试时无可用凭证或令牌") yield Response( content=json.dumps({"error": "当前无可用凭证"}), @@ -380,9 +406,10 @@ async def non_stream_request( DISABLE_ERROR_CODES = await get_auto_ban_error_codes() # 禁用凭证的错误码 last_error_response = None # 记录最后一次的错误响应 - - # 内部函数:获取新凭证并更新headers - async def refresh_credential(): + next_cred_task = None # 预热的下一个凭证任务 + + # 内部函数:快速更新凭证(只更新token和project_id,避免重建整个请求) + async def refresh_credential_fast(): nonlocal current_file, access_token, auth_headers, project_id, final_payload cred_result = await credential_manager.get_valid_credential( mode="antigravity", model_key=model_name @@ -394,10 +421,9 @@ async def refresh_credential(): project_id = credential_data.get("project_id", "") if not access_token: return None - auth_headers = build_antigravity_headers(access_token, model_name) - if headers: - auth_headers.update(headers) - final_payload = {"model": body.get("model"), "project": project_id, "request": body.get("request", {})} + # 只更新token和project_id,不重建整个headers和payload + auth_headers["Authorization"] = f"Bearer {access_token}" + final_payload["project"] = project_id return True for attempt in range(max_retries + 1): @@ -454,19 +480,29 @@ async def refresh_credential(): ) # 判断是否需要重试 + # 缓存错误文本,避免重复解析 + error_text = "" + try: + error_text = response.text + except Exception: + pass + if status_code == 429 or status_code in DISABLE_ERROR_CODES: - try: - error_text = response.text - log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") - except Exception: - log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}") + log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500] if error_text else '无'}") + + # 并行预热下一个凭证,不阻塞当前处理 + if next_cred_task is None and attempt < max_retries: + next_cred_task = asyncio.create_task( + credential_manager.get_valid_credential( + mode="antigravity", model_key=model_name + ) + ) # 记录错误 cooldown_until = None - if status_code == 429: - # 尝试解析冷却时间 + if status_code == 429 and error_text: + # 使用已缓存的error_text解析冷却时间 try: - error_text = response.text cooldown_until = await parse_and_log_cooldown(error_text, mode="antigravity") except Exception: pass @@ -491,11 +527,7 @@ async def refresh_credential(): return last_error_response else: # 错误码不在禁用码当中,直接返回,无需重试 - try: - error_text = response.text - log.error(f"[ANTIGRAVITY] 非流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}") - except Exception: - log.error(f"[ANTIGRAVITY] 非流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}") + log.error(f"[ANTIGRAVITY] 非流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500] if error_text else '无'}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="antigravity", model_key=model_name @@ -505,9 +537,30 @@ async def refresh_credential(): # 统一处理重试 if need_retry: log.info(f"[ANTIGRAVITY] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") + + # 使用预热的凭证任务,避免等待 + if next_cred_task is not None: + try: + cred_result = await next_cred_task + next_cred_task = None # 重置任务 + + if cred_result: + current_file, credential_data = cred_result + access_token = credential_data.get("access_token") or credential_data.get("token") + project_id = credential_data.get("project_id", "") + if access_token and project_id: + auth_headers["Authorization"] = f"Bearer {access_token}" + final_payload["project"] = project_id + await asyncio.sleep(retry_interval) + continue # 重试 + except Exception as e: + log.warning(f"[ANTIGRAVITY] 预热凭证任务失败: {e}") + next_cred_task = None + + # 如果预热的凭证不可用,则同步获取 await asyncio.sleep(retry_interval) - - if not await refresh_credential(): + + if not await refresh_credential_fast(): log.error("[ANTIGRAVITY] 重试时无可用凭证或令牌") return Response( content=json.dumps({"error": "当前无可用凭证"}), diff --git a/src/api/geminicli.py b/src/api/geminicli.py index c99a319e5..108ce6755 100644 --- a/src/api/geminicli.py +++ b/src/api/geminicli.py @@ -148,10 +148,11 @@ async def stream_request( DISABLE_ERROR_CODES = await get_auto_ban_error_codes() # 禁用凭证的错误码 last_error_response = None # 记录最后一次的错误响应 - - # 内部函数:获取新凭证并更新headers - async def refresh_credential(): - nonlocal current_file, credential_data, auth_headers, final_payload, target_url + next_cred_task = None # 预热的下一个凭证任务 + + # 内部函数:快速更新凭证(只更新token和project_id,避免重建整个请求) + async def refresh_credential_fast(): + nonlocal current_file, credential_data, auth_headers, final_payload cred_result = await credential_manager.get_valid_credential( mode="geminicli", model_key=model_group ) @@ -159,12 +160,15 @@ async def refresh_credential(): return None current_file, credential_data = cred_result try: - auth_headers, final_payload, target_url = await prepare_request_headers_and_payload( - body, credential_data, - f"{await get_code_assist_endpoint()}/v1internal:streamGenerateContent?alt=sse" - ) - if headers: - auth_headers.update(headers) + # 只更新token和project_id,不重建整个headers和payload + token = credential_data.get("token") or credential_data.get("access_token", "") + project_id = credential_data.get("project_id", "") + if not token or not project_id: + return None + + # 直接更新现有的headers和payload + auth_headers["Authorization"] = f"Bearer {token}" + final_payload["project"] = project_id return True except Exception: return None @@ -185,21 +189,30 @@ async def refresh_credential(): status_code = chunk.status_code last_error_response = chunk # 记录最后一次错误 + # 缓存错误解析结果,避免重复decode + error_body = None + try: + error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) + except Exception: + error_body = "" + # 如果错误码是429或者在禁用码当中,做好记录后进行重试 if status_code == 429 or status_code in DISABLE_ERROR_CODES: - # 解析错误响应内容 - try: - error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) - log.warning(f"[GEMINICLI STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") - except Exception: - log.warning(f"[GEMINICLI STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}") + log.warning(f"[GEMINICLI STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500] if error_body else '无'}") + + # 并行预热下一个凭证,不阻塞当前处理 + if next_cred_task is None and attempt < max_retries: + next_cred_task = asyncio.create_task( + credential_manager.get_valid_credential( + mode="geminicli", model_key=model_group + ) + ) # 记录错误 cooldown_until = None - if status_code == 429: - # 尝试解析冷却时间 + if status_code == 429 and error_body: + # 使用已缓存的error_body解析冷却时间 try: - error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) cooldown_until = await parse_and_log_cooldown(error_body, mode="geminicli") except Exception: pass @@ -226,11 +239,7 @@ async def refresh_credential(): return else: # 错误码不在禁用码当中,直接返回,无需重试 - try: - error_body = chunk.body.decode('utf-8') if isinstance(chunk.body, bytes) else str(chunk.body) - log.error(f"[GEMINICLI STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500]}") - except Exception: - log.error(f"[GEMINICLI STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}") + log.error(f"[GEMINICLI STREAM] 流式请求失败,非重试错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500] if error_body else '无'}") await record_api_call_error( credential_manager, current_file, status_code, None, mode="geminicli", model_key=model_group @@ -275,9 +284,31 @@ async def refresh_credential(): # 统一处理重试 if need_retry: log.info(f"[GEMINICLI STREAM] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") + + # 使用预热的凭证任务,避免等待 + if next_cred_task is not None: + try: + cred_result = await next_cred_task + next_cred_task = None # 重置任务 + + if cred_result: + current_file, credential_data = cred_result + # 使用快速更新方式 + token = credential_data.get("token") or credential_data.get("access_token", "") + project_id = credential_data.get("project_id", "") + if token and project_id: + auth_headers["Authorization"] = f"Bearer {token}" + final_payload["project"] = project_id + await asyncio.sleep(retry_interval) + continue # 重试 + except Exception as e: + log.warning(f"[GEMINICLI STREAM] 预热凭证任务失败: {e}") + next_cred_task = None + + # 如果预热的凭证不可用,则同步获取 await asyncio.sleep(retry_interval) - - if not await refresh_credential(): + + if not await refresh_credential_fast(): log.error("[GEMINICLI STREAM] 重试时无可用凭证或刷新失败") yield Response( content=json.dumps({"error": "当前无可用凭证"}), @@ -359,6 +390,30 @@ async def non_stream_request( DISABLE_ERROR_CODES = await get_auto_ban_error_codes() # 禁用凭证的错误码 last_error_response = None # 记录最后一次的错误响应 + next_cred_task = None # 预热的下一个凭证任务 + + # 内部函数:快速更新凭证(只更新token和project_id,避免重建整个请求) + async def refresh_credential_fast(): + nonlocal current_file, credential_data, auth_headers, final_payload + cred_result = await credential_manager.get_valid_credential( + mode="geminicli", model_key=model_group + ) + if not cred_result: + return None + current_file, credential_data = cred_result + try: + # 只更新token和project_id,不重建整个headers和payload + token = credential_data.get("token") or credential_data.get("access_token", "") + project_id = credential_data.get("project_id", "") + if not token or not project_id: + return None + + # 直接更新现有的headers和payload + auth_headers["Authorization"] = f"Bearer {token}" + final_payload["project"] = project_id + return True + except Exception: + return None for attempt in range(max_retries + 1): try: @@ -400,17 +455,25 @@ async def non_stream_request( ) # 判断是否需要重试 - # 获取错误文本 + # 缓存错误文本,避免重复解析 + error_text = "" try: error_text = response.text - error_msg = f"非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500]}" except Exception: - error_text = "" - error_msg = f"非流式请求失败 (status={status_code}), 凭证: {current_file}" + pass # 如果错误码在禁用码当中,禁用该凭证 if status_code in DISABLE_ERROR_CODES: log.error(f"非流式请求失败,禁用错误码 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500] if error_text else '无'}") + + # 并行预热下一个凭证,不阻塞当前处理 + if next_cred_task is None and attempt < max_retries: + next_cred_task = asyncio.create_task( + credential_manager.get_valid_credential( + mode="geminicli", model_key=model_group + ) + ) + # 记录错误并禁用凭证 await record_api_call_error( credential_manager, current_file, status_code, @@ -419,27 +482,37 @@ async def non_stream_request( # 尝试切换到新凭证并重试 if attempt < max_retries: log.info(f"[NON-STREAM] 切换凭证并重试 (attempt {attempt + 2}/{max_retries + 1})...") + + # 使用预热的凭证任务,避免等待 + if next_cred_task is not None: + try: + cred_result = await next_cred_task + next_cred_task = None # 重置任务 + + if cred_result: + current_file, credential_data = cred_result + # 使用快速更新方式 + token = credential_data.get("token") or credential_data.get("access_token", "") + project_id = credential_data.get("project_id", "") + if token and project_id: + auth_headers["Authorization"] = f"Bearer {token}" + final_payload["project"] = project_id + await asyncio.sleep(retry_interval) + continue # 重试 + except Exception as e: + log.warning(f"[NON-STREAM] 预热凭证任务失败: {e}") + next_cred_task = None + + # 如果预热的凭证不可用,则同步获取 await asyncio.sleep(retry_interval) - # 获取新凭证 - cred_result = await credential_manager.get_valid_credential( - mode="geminicli", model_key=model_group - ) - if not cred_result: - log.error("[NON-STREAM] 重试时无可用凭证") + if not await refresh_credential_fast(): + log.error("[NON-STREAM] 重试时无可用凭证或刷新失败") return Response( content=json.dumps({"error": "当前无可用凭证"}), status_code=500, media_type="application/json" ) - - current_file, credential_data = cred_result - auth_headers, final_payload, target_url = await prepare_request_headers_and_payload( - body, credential_data, - f"{await get_code_assist_endpoint()}/v1internal:generateContent" - ) - if headers: - auth_headers.update(headers) continue # 重试 else: # 达到最大重试次数 @@ -447,15 +520,21 @@ async def non_stream_request( return last_error_response else: # 错误码不在禁用码当中(如429等),做好记录后进行重试 - log.warning(error_msg) + log.warning(f"非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500] if error_text else '无'}") + + # 并行预热下一个凭证,不阻塞当前处理 + if next_cred_task is None and attempt < max_retries: + next_cred_task = asyncio.create_task( + credential_manager.get_valid_credential( + mode="geminicli", model_key=model_group + ) + ) # 记录错误 cooldown_until = None - if status_code == 429: - # 尝试解析冷却时间 + if status_code == 429 and error_text: + # 使用已缓存的error_text解析冷却时间 try: - if not error_text: - error_text = response.text cooldown_until = await parse_and_log_cooldown(error_text, mode="geminicli") except Exception: pass @@ -475,27 +554,37 @@ async def non_stream_request( if should_retry and attempt < max_retries: # 重新获取凭证并重试 log.info(f"[NON-STREAM] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") + + # 使用预热的凭证任务,避免等待 + if next_cred_task is not None: + try: + cred_result = await next_cred_task + next_cred_task = None # 重置任务 + + if cred_result: + current_file, credential_data = cred_result + # 使用快速更新方式 + token = credential_data.get("token") or credential_data.get("access_token", "") + project_id = credential_data.get("project_id", "") + if token and project_id: + auth_headers["Authorization"] = f"Bearer {token}" + final_payload["project"] = project_id + await asyncio.sleep(retry_interval) + continue # 重试 + except Exception as e: + log.warning(f"[NON-STREAM] 预热凭证任务失败: {e}") + next_cred_task = None + + # 如果预热的凭证不可用,则同步获取 await asyncio.sleep(retry_interval) - # 获取新凭证 - cred_result = await credential_manager.get_valid_credential( - mode="geminicli", model_key=model_group - ) - if not cred_result: - log.error("[NON-STREAM] 重试时无可用凭证") + if not await refresh_credential_fast(): + log.error("[NON-STREAM] 重试时无可用凭证或刷新失败") return Response( content=json.dumps({"error": "当前无可用凭证"}), status_code=500, media_type="application/json" ) - - current_file, credential_data = cred_result - auth_headers, final_payload, target_url = await prepare_request_headers_and_payload( - body, credential_data, - f"{await get_code_assist_endpoint()}/v1internal:generateContent" - ) - if headers: - auth_headers.update(headers) continue # 重试 else: # 不重试,直接返回原始错误 From 4528b34548fea57e328e91a8fa6ae4fa30a9c144 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jan 2026 15:08:26 +0000 Subject: [PATCH 134/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 34a2292fe..450f2242d 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=36d7ceef61a311837bb57fa66254abfca0bd1742 -short_hash=36d7cee -message=Update gemini_fix.py -date=2026-01-14 21:55:29 +0800 +full_hash=160294d9afdd47f12bc469b7dd8ab9feb92db866 +short_hash=160294d +message=优化重试速度 +date=2026-01-14 23:08:17 +0800 From 16e30369c873ebe4f013331185a119da62ba322d Mon Sep 17 00:00:00 2001 From: MIKUSCAT Date: Thu, 15 Jan 2026 12:39:06 +0800 Subject: [PATCH 135/211] =?UTF-8?q?fix:=20stream=20collector=20=E4=BF=9D?= =?UTF-8?q?=E7=95=99=20functionCall/functionResponse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:stream=false 时,stream2nostream 收集器会丢弃工具调用相关的 parts, 导致非流式模式下工具调用无法正常工作。 修复: - 新增 collected_tool_parts 收集 functionCall/functionResponse - functionCall 按 id 去重,保留最后一次更新(处理流式分块更新 args) - 合并响应时将工具 parts 加入 final_parts Co-Authored-By: Claude Opus 4.5 --- src/api/utils.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/api/utils.py b/src/api/utils.py index 491ac2537..de4d7ca53 100644 --- a/src/api/utils.py +++ b/src/api/utils.py @@ -261,6 +261,8 @@ async def collect_streaming_response(stream_generator) -> Response: collected_text = [] # 用于收集文本内容 collected_thought_text = [] # 用于收集思维链内容 + collected_tool_parts = [] # 用于收集 functionCall/functionResponse 等工具相关结构化内容 + function_call_index_by_id = {} # functionCall 按 id 去重/更新(流式可能分块更新 args) collected_other_parts = [] # 用于收集其他类型的parts(图片、文件等) has_data = False line_count = 0 @@ -326,6 +328,24 @@ async def collect_streaming_response(stream_generator) -> Response: if not isinstance(part, dict): continue + # 处理工具调用/响应(非文本结构化 part) + if "functionCall" in part or "functionResponse" in part: + # functionCall 可能会在流里重复出现:按 id 去重,但保留“最后一次”的内容(防止分块更新 args) + if "functionCall" in part: + function_call = part.get("functionCall") + if isinstance(function_call, dict): + call_id = function_call.get("id") + if call_id: + if call_id in function_call_index_by_id: + collected_tool_parts[function_call_index_by_id[call_id]] = part + log.debug(f"[STREAM COLLECTOR] Updated functionCall part by id: {call_id}") + continue + function_call_index_by_id[call_id] = len(collected_tool_parts) + + collected_tool_parts.append(part) + log.debug(f"[STREAM COLLECTOR] Collected tool part: {list(part.keys())}") + continue + # 处理文本内容 text = part.get("text", "") if text: @@ -399,6 +419,8 @@ async def collect_streaming_response(stream_generator) -> Response: }) # 添加其他类型的parts(图片、文件等) + # 先追加工具相关结构化 parts(functionCall/functionResponse),否则工具调用会丢失 + final_parts.extend(collected_tool_parts) final_parts.extend(collected_other_parts) # 如果没有任何内容,添加空文本 @@ -407,7 +429,10 @@ async def collect_streaming_response(stream_generator) -> Response: merged_response["response"]["candidates"][0]["content"]["parts"] = final_parts - log.info(f"[STREAM COLLECTOR] Collected {len(collected_text)} text chunks, {len(collected_thought_text)} thought chunks, and {len(collected_other_parts)} other parts") + log.info( + f"[STREAM COLLECTOR] Collected {len(collected_text)} text chunks, {len(collected_thought_text)} thought chunks, " + f"{len(collected_tool_parts)} tool parts, and {len(collected_other_parts)} other parts" + ) # 去掉嵌套的 "response" 包装(Antigravity格式 -> 标准Gemini格式) if "response" in merged_response and "candidates" not in merged_response: @@ -494,4 +519,4 @@ def get_model_group(model_name: str) -> str: return "flash" else: # pro 模型(包括 gemini-2.5-pro 和 gemini-3-pro-preview) - return "pro" \ No newline at end of file + return "pro" From fb09b68ddf122149f2e2e7c40bcc6c5b1843f4cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Jan 2026 06:24:19 +0000 Subject: [PATCH 136/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 450f2242d..d55c5d293 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=160294d9afdd47f12bc469b7dd8ab9feb92db866 -short_hash=160294d -message=优化重试速度 -date=2026-01-14 23:08:17 +0800 +full_hash=b687998ef3f070a1e1fc0ed60ef1a2d7b4d44c88 +short_hash=b687998 +message=Merge pull request #284 from MIKUSCAT/fix/stream-collector-tool-calls +date=2026-01-15 14:24:08 +0800 From c5ee7308fe4474a6270497f43a9e0a6319d84ea7 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Thu, 15 Jan 2026 18:00:19 +0800 Subject: [PATCH 137/211] =?UTF-8?q?=E5=BD=93=E6=B2=A1=E6=9C=89=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=88=B0=E6=80=9D=E8=80=83=E7=AD=BE=E5=90=8D=E6=97=B6?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E5=A1=AB=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 4 +++- src/converter/openai2gemini.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index 9a196288d..7ac12760e 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -478,9 +478,11 @@ def convert_messages_to_contents( } } - # 如果提取到签名则添加 + # 如果提取到签名则添加,否则使用占位符以满足 Gemini API 要求 if signature: fc_part["thoughtSignature"] = signature + else: + fc_part["thoughtSignature"] = "skip_thought_signature_validator" parts.append(fc_part) elif item_type == "tool_result": diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 928539fe7..5531ad31c 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -1058,10 +1058,12 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di "args": args } } - - # 如果有thoughtSignature,添加到part中 + + # 如果有thoughtSignature则添加,否则使用占位符以满足 Gemini API 要求 if signature: function_call_part["thoughtSignature"] = signature + else: + function_call_part["thoughtSignature"] = "skip_thought_signature_validator" parts.append(function_call_part) except (json.JSONDecodeError, KeyError) as e: From d6eb88a9fc7614118a74a29374734e90b65fffce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Jan 2026 10:00:37 +0000 Subject: [PATCH 138/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index d55c5d293..4b447b8e0 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=b687998ef3f070a1e1fc0ed60ef1a2d7b4d44c88 -short_hash=b687998 -message=Merge pull request #284 from MIKUSCAT/fix/stream-collector-tool-calls -date=2026-01-15 14:24:08 +0800 +full_hash=0c0f7e24470cbdd1b88e1fa05a36919aa0439331 +short_hash=0c0f7e2 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-15 18:00:22 +0800 From 5210dfee8301b0f9996ff059f0f76fff4aded8d2 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Thu, 15 Jan 2026 22:51:32 +0800 Subject: [PATCH 139/211] =?UTF-8?q?Revert=20"fix:=20stream=20collector=20?= =?UTF-8?q?=E4=BF=9D=E7=95=99=20functionCall/functionResponse"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 16e30369c873ebe4f013331185a119da62ba322d. --- src/api/utils.py | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/src/api/utils.py b/src/api/utils.py index de4d7ca53..491ac2537 100644 --- a/src/api/utils.py +++ b/src/api/utils.py @@ -261,8 +261,6 @@ async def collect_streaming_response(stream_generator) -> Response: collected_text = [] # 用于收集文本内容 collected_thought_text = [] # 用于收集思维链内容 - collected_tool_parts = [] # 用于收集 functionCall/functionResponse 等工具相关结构化内容 - function_call_index_by_id = {} # functionCall 按 id 去重/更新(流式可能分块更新 args) collected_other_parts = [] # 用于收集其他类型的parts(图片、文件等) has_data = False line_count = 0 @@ -328,24 +326,6 @@ async def collect_streaming_response(stream_generator) -> Response: if not isinstance(part, dict): continue - # 处理工具调用/响应(非文本结构化 part) - if "functionCall" in part or "functionResponse" in part: - # functionCall 可能会在流里重复出现:按 id 去重,但保留“最后一次”的内容(防止分块更新 args) - if "functionCall" in part: - function_call = part.get("functionCall") - if isinstance(function_call, dict): - call_id = function_call.get("id") - if call_id: - if call_id in function_call_index_by_id: - collected_tool_parts[function_call_index_by_id[call_id]] = part - log.debug(f"[STREAM COLLECTOR] Updated functionCall part by id: {call_id}") - continue - function_call_index_by_id[call_id] = len(collected_tool_parts) - - collected_tool_parts.append(part) - log.debug(f"[STREAM COLLECTOR] Collected tool part: {list(part.keys())}") - continue - # 处理文本内容 text = part.get("text", "") if text: @@ -419,8 +399,6 @@ async def collect_streaming_response(stream_generator) -> Response: }) # 添加其他类型的parts(图片、文件等) - # 先追加工具相关结构化 parts(functionCall/functionResponse),否则工具调用会丢失 - final_parts.extend(collected_tool_parts) final_parts.extend(collected_other_parts) # 如果没有任何内容,添加空文本 @@ -429,10 +407,7 @@ async def collect_streaming_response(stream_generator) -> Response: merged_response["response"]["candidates"][0]["content"]["parts"] = final_parts - log.info( - f"[STREAM COLLECTOR] Collected {len(collected_text)} text chunks, {len(collected_thought_text)} thought chunks, " - f"{len(collected_tool_parts)} tool parts, and {len(collected_other_parts)} other parts" - ) + log.info(f"[STREAM COLLECTOR] Collected {len(collected_text)} text chunks, {len(collected_thought_text)} thought chunks, and {len(collected_other_parts)} other parts") # 去掉嵌套的 "response" 包装(Antigravity格式 -> 标准Gemini格式) if "response" in merged_response and "candidates" not in merged_response: @@ -519,4 +494,4 @@ def get_model_group(model_name: str) -> str: return "flash" else: # pro 模型(包括 gemini-2.5-pro 和 gemini-3-pro-preview) - return "pro" + return "pro" \ No newline at end of file From 6da5f34dd3b244616b1af93274e80c5c1226b6ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Jan 2026 14:52:06 +0000 Subject: [PATCH 140/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 4b447b8e0..59e5f782c 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=0c0f7e24470cbdd1b88e1fa05a36919aa0439331 -short_hash=0c0f7e2 -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-15 18:00:22 +0800 +full_hash=5210dfee8301b0f9996ff059f0f76fff4aded8d2 +short_hash=5210dfe +message=Revert "fix: stream collector 保留 functionCall/functionResponse" +date=2026-01-15 22:51:32 +0800 From a73f272fd3768a051026611b64cd6015fef11c0e Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Thu, 15 Jan 2026 23:52:43 +0800 Subject: [PATCH 141/211] =?UTF-8?q?=E8=AE=A9claude=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E6=9B=B4=E5=A5=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/anthropic2gemini.py | 114 +++++++++++++++--------------- src/converter/gemini_fix.py | 44 +----------- 2 files changed, 60 insertions(+), 98 deletions(-) diff --git a/src/converter/anthropic2gemini.py b/src/converter/anthropic2gemini.py index 7ac12760e..80d53f8b5 100644 --- a/src/converter/anthropic2gemini.py +++ b/src/converter/anthropic2gemini.py @@ -29,7 +29,7 @@ MIN_SIGNATURE_LENGTH = 10 -def has_valid_signature(block: Dict[str, Any]) -> bool: +def has_valid_thoughtsignature(block: Dict[str, Any]) -> bool: """ 检查 thinking 块是否有有效签名 @@ -47,14 +47,14 @@ def has_valid_signature(block: Dict[str, Any]) -> bool: return True # 非 thinking 块默认有效 thinking = block.get("thinking", "") - signature = block.get("signature") + thoughtsignature = block.get("thoughtSignature") - # 空 thinking + 任意 signature = 有效 (trailing signature case) - if not thinking and signature is not None: + # 空 thinking + 任意 thoughtsignature = 有效 (trailing signature case) + if not thinking and thoughtsignature is not None: return True - # 有内容 + 足够长度的 signature = 有效 - if signature and isinstance(signature, str) and len(signature) >= MIN_SIGNATURE_LENGTH: + # 有内容 + 足够长度的 thoughtsignature = 有效 + if thoughtsignature and isinstance(thoughtsignature, str) and len(thoughtsignature) >= MIN_SIGNATURE_LENGTH: return True return False @@ -83,9 +83,9 @@ def sanitize_thinking_block(block: Dict[str, Any]) -> Dict[str, Any]: "thinking": block.get("thinking", "") } - signature = block.get("signature") - if signature: - sanitized["signature"] = signature + thoughtsignature = block.get("thoughtSignature") + if thoughtsignature: + sanitized["thoughtSignature"] = thoughtsignature return sanitized @@ -109,7 +109,7 @@ def remove_trailing_unsigned_thinking(blocks: List[Dict[str, Any]]) -> None: block_type = block.get("type") if block_type in ("thinking", "redacted_thinking"): - if not has_valid_signature(block): + if not has_valid_thoughtsignature(block): end_index = i else: break # 遇到有效签名的 thinking 块,停止 @@ -124,38 +124,39 @@ def remove_trailing_unsigned_thinking(blocks: List[Dict[str, Any]]) -> None: def filter_invalid_thinking_blocks(messages: List[Dict[str, Any]]) -> None: """ - 过滤消息中的无效 thinking 块 - + 过滤消息中的无效 thinking 块,并清理所有 thinking 块的额外字段(如 cache_control) + Args: messages: Anthropic messages 列表 (会被修改) """ total_filtered = 0 - + for msg in messages: # 只处理 assistant 和 model 消息 role = msg.get("role", "") if role not in ("assistant", "model"): continue - + content = msg.get("content") if not isinstance(content, list): continue - + original_len = len(content) new_blocks: List[Dict[str, Any]] = [] - + for block in content: if not isinstance(block, dict): new_blocks.append(block) continue - + block_type = block.get("type") if block_type not in ("thinking", "redacted_thinking"): new_blocks.append(block) continue - + + # 所有 thinking 块都需要清理(移除 cache_control 等额外字段) # 检查 thinking 块的有效性 - if has_valid_signature(block): + if has_valid_thoughtsignature(block): # 有效签名,清理后保留 new_blocks.append(sanitize_thinking_block(block)) else: @@ -163,21 +164,21 @@ def filter_invalid_thinking_blocks(messages: List[Dict[str, Any]]) -> None: thinking_text = block.get("thinking", "") if thinking_text and str(thinking_text).strip(): log.info( - f"[Claude-Handler] Converting thinking block with invalid signature to text. " + f"[Claude-Handler] Converting thinking block with invalid thoughtSignature to text. " f"Content length: {len(thinking_text)} chars" ) new_blocks.append({"type": "text", "text": thinking_text}) else: - log.debug("[Claude-Handler] Dropping empty thinking block with invalid signature") - + log.debug("[Claude-Handler] Dropping empty thinking block with invalid thoughtSignature") + msg["content"] = new_blocks filtered_count = original_len - len(new_blocks) total_filtered += filtered_count - + # 如果过滤后为空,添加一个空文本块以保持消息有效 if not new_blocks: msg["content"] = [{"type": "text", "text": ""}] - + if total_filtered > 0: log.debug(f"Filtered {total_filtered} invalid thinking block(s) from history") @@ -374,7 +375,7 @@ def convert_messages_to_contents( """ contents: List[Dict[str, Any]] = [] - # 第一遍:构建 tool_use_id -> (name, signature) 的映射 + # 第一遍:构建 tool_use_id -> (name, thoughtsignature) 的映射 # 注意:存储的是编码后的 ID(可能包含签名) tool_use_info: Dict[str, tuple[str, Optional[str]]] = {} for msg in messages: @@ -386,9 +387,9 @@ def convert_messages_to_contents( tool_name = item.get("name") if encoded_tool_id and tool_name: # 解码获取原始ID和签名 - original_id, signature = decode_tool_id_and_signature(encoded_tool_id) - # 存储映射:编码ID -> (name, signature) - tool_use_info[str(encoded_tool_id)] = (tool_name, signature) + original_id, thoughtsignature = decode_tool_id_and_signature(encoded_tool_id) + # 存储映射:编码ID -> (name, thoughtsignature) + tool_use_info[str(encoded_tool_id)] = (tool_name, thoughtsignature) for msg in messages: role = msg.get("role", "user") @@ -426,10 +427,10 @@ def convert_messages_to_contents( "thought": True, } - # 如果有 signature 则添加 - signature = item.get("signature") - if signature: - part["thoughtSignature"] = signature + # 如果有 thoughtsignature 则添加 + thoughtsignature = item.get("thoughtSignature") + if thoughtsignature: + part["thoughtSignature"] = thoughtsignature parts.append(part) elif item_type == "redacted_thinking": @@ -445,10 +446,10 @@ def convert_messages_to_contents( "thought": True, } - # 如果有 signature 则添加 - signature = item.get("signature") - if signature: - part_dict["thoughtSignature"] = signature + # 如果有 thoughtsignature 则添加 + thoughtsignature = item.get("thoughtSignature") + if thoughtsignature: + part_dict["thoughtSignature"] = thoughtsignature parts.append(part_dict) elif item_type == "text": @@ -468,7 +469,7 @@ def convert_messages_to_contents( ) elif item_type == "tool_use": encoded_id = item.get("id") or "" - original_id, signature = decode_tool_id_and_signature(encoded_id) + original_id, thoughtsignature = decode_tool_id_and_signature(encoded_id) fc_part: Dict[str, Any] = { "functionCall": { @@ -479,8 +480,8 @@ def convert_messages_to_contents( } # 如果提取到签名则添加,否则使用占位符以满足 Gemini API 要求 - if signature: - fc_part["thoughtSignature"] = signature + if thoughtsignature: + fc_part["thoughtSignature"] = thoughtsignature else: fc_part["thoughtSignature"] = "skip_thought_signature_validator" @@ -827,10 +828,10 @@ def gemini_to_anthropic_response( block: Dict[str, Any] = {"type": "thinking", "thinking": str(thinking_text)} - # 如果有 signature 则添加 - signature = part.get("thoughtSignature") - if signature: - block["signature"] = signature + # 如果有 thoughtsignature 则添加 + thoughtsignature = part.get("thoughtSignature") + if thoughtsignature: + block["thoughtSignature"] = thoughtsignature content.append(block) continue @@ -845,10 +846,10 @@ def gemini_to_anthropic_response( has_tool_use = True fc = part.get("functionCall", {}) or {} original_id = fc.get("id") or f"toolu_{uuid.uuid4().hex}" - signature = part.get("thoughtSignature") + thoughtsignature = part.get("thoughtSignature") # 对工具调用ID进行签名编码 - encoded_id = encode_tool_id_with_signature(original_id, signature) + encoded_id = encode_tool_id_with_signature(original_id, thoughtsignature) content.append( { "type": "tool_use", @@ -1032,7 +1033,7 @@ def _close_block() -> Optional[bytes]: # 处理 thinking 块 if part.get("thought") is True: thinking_text = part.get("text", "") - signature = part.get("thoughtSignature") + thoughtsignature = part.get("thoughtSignature") # 检查是否需要关闭上一个块并开启新的 thinking 块 if current_block_type != "thinking": @@ -1042,12 +1043,11 @@ def _close_block() -> Optional[bytes]: current_block_index += 1 current_block_type = "thinking" - current_thinking_signature = signature + current_thinking_signature = thoughtsignature block: Dict[str, Any] = {"type": "thinking", "thinking": ""} - if signature: - block["signature"] = signature - + if thoughtsignature: + block["thoughtSignature"] = thoughtsignature yield _sse_event( "content_block_start", { @@ -1056,7 +1056,7 @@ def _close_block() -> Optional[bytes]: "content_block": block, }, ) - elif signature and signature != current_thinking_signature: + elif thoughtsignature and thoughtsignature != current_thinking_signature: # 签名变化,需要开启新的 thinking 块 close_evt = _close_block() if close_evt: @@ -1064,11 +1064,11 @@ def _close_block() -> Optional[bytes]: current_block_index += 1 current_block_type = "thinking" - current_thinking_signature = signature + current_thinking_signature = thoughtsignature block_new: Dict[str, Any] = {"type": "thinking", "thinking": ""} - if signature: - block_new["signature"] = signature + if thoughtsignature: + block_new["thoughtSignature"] = thoughtsignature yield _sse_event( "content_block_start", @@ -1134,15 +1134,15 @@ def _close_block() -> Optional[bytes]: has_tool_use = True fc = part.get("functionCall", {}) or {} original_id = fc.get("id") or f"toolu_{uuid.uuid4().hex}" - signature = part.get("thoughtSignature") - tool_id = encode_tool_id_with_signature(original_id, signature) + thoughtsignature = part.get("thoughtSignature") + tool_id = encode_tool_id_with_signature(original_id, thoughtsignature) tool_name = fc.get("name") or "" tool_args = _remove_nulls_for_tool_input(fc.get("args", {}) or {}) if _anthropic_debug_enabled(): log.info( f"[ANTHROPIC][tool_use] 处理工具调用: name={tool_name}, " - f"id={tool_id}, has_signature={signature is not None}" + f"id={tool_id}, has_signature={thoughtsignature is not None}" ) current_block_index += 1 diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index d9b16f862..aa7512dad 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -113,44 +113,6 @@ def is_thinking_model(model_name: str) -> bool: return "think" in model_name or "pro" in model_name.lower() -def check_last_assistant_has_thinking(contents: List[Dict[str, Any]]) -> bool: - """ - 检查最后一个 assistant 消息是否以 thinking 块开始 - - 根据 Claude API 要求:当启用 thinking 时,最后一个 assistant 消息必须以 thinking 块开始 - - Args: - contents: Gemini 格式的 contents 数组 - - Returns: - 如果最后一个 assistant 消息以 thinking 块开始则返回 True,否则返回 False - """ - if not contents: - return True # 没有 contents,允许启用 thinking - - # 从后往前找最后一个 assistant (model) 消息 - last_assistant_content = None - for content in reversed(contents): - if isinstance(content, dict) and content.get("role") == "model": - last_assistant_content = content - break - - if not last_assistant_content: - return True # 没有 assistant 消息,允许启用 thinking - - # 检查第一个 part 是否是 thinking 块 - parts = last_assistant_content.get("parts", []) - if not parts: - return False # 有 assistant 消息但没有 parts,不允许 thinking - - first_part = parts[0] - if not isinstance(first_part, dict): - return False - - # 检查是否是 thinking 块(有 thought 或 thoughtSignature 字段) - return "thought" in first_part or "thoughtSignature" in first_part - - async def normalize_gemini_request( request: Dict[str, Any], mode: str = "geminicli" @@ -255,7 +217,7 @@ async def normalize_gemini_request( # 检查最后一个 assistant 消息是否以 thinking 块开始 contents = result.get("contents", []) - if not check_last_assistant_has_thinking(contents) and "claude" in model.lower(): + if "claude" in model.lower(): # 检测是否有工具调用(MCP场景) has_tool_calls = any( isinstance(content, dict) and @@ -272,7 +234,7 @@ async def normalize_gemini_request( generation_config.pop("thinkingConfig", None) else: # 非 MCP 场景:填充思考块 - log.warning(f"[ANTIGRAVITY] 最后一个 assistant 消息不以 thinking 块开始,自动填充思考块") + # log.warning(f"[ANTIGRAVITY] 最后一个 assistant 消息不以 thinking 块开始,自动填充思考块") # 找到最后一个 model 角色的 content for i in range(len(contents) - 1, -1, -1): @@ -281,7 +243,7 @@ async def normalize_gemini_request( # 在 parts 开头插入思考块(使用官方跳过验证的虚拟签名) parts = content.get("parts", []) thinking_part = { - "text": "Continuing from previous context...", + "text": "...", # "thought": True, # 标记为思考块 "thoughtSignature": "skip_thought_signature_validator" # 官方文档推荐的虚拟签名 } From 2c59dd6c40e8fa701cce24f252a53d4a512db6f7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 15 Jan 2026 15:52:54 +0000 Subject: [PATCH 142/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 59e5f782c..05db720d5 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=5210dfee8301b0f9996ff059f0f76fff4aded8d2 -short_hash=5210dfe -message=Revert "fix: stream collector 保留 functionCall/functionResponse" -date=2026-01-15 22:51:32 +0800 +full_hash=a73f272fd3768a051026611b64cd6015fef11c0e +short_hash=a73f272 +message=让claude格式工具调用更好 +date=2026-01-15 23:52:43 +0800 From afc3708e53c7e73bc62d975439c5b50df4a4401e Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Thu, 15 Jan 2026 23:54:39 +0800 Subject: [PATCH 143/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index aa7512dad..48e40eb78 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -4,7 +4,7 @@ ──────────────────────────────────────────────────────────────── """ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from log import log from src.utils import DEFAULT_SAFETY_SETTINGS From 84656164d3fe36d9dac40bb6f1fbf59fc6e1a79e Mon Sep 17 00:00:00 2001 From: Dongmayyys <89748169+Dongmayyys@users.noreply.github.com> Date: Sat, 17 Jan 2026 12:45:24 +0800 Subject: [PATCH 144/211] fix: remove thinkingLevel when thinkingBudget is set to avoid 400 error --- src/converter/gemini_fix.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index aa7512dad..6675193a0 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -168,6 +168,7 @@ async def normalize_gemini_request( # 设置思考预算 if thinking_budget: thinking_config["thinkingBudget"] = thinking_budget + thinking_config.pop("thinkingLevel", None) # 避免与 thinkingBudget 冲突 # includeThoughts 使用配置值 thinking_config["includeThoughts"] = return_thoughts @@ -212,6 +213,7 @@ async def normalize_gemini_request( # 优先使用传入的思考预算,否则使用默认值 if "thinkingBudget" not in thinking_config: thinking_config["thinkingBudget"] = 1024 + thinking_config.pop("thinkingLevel", None) # 避免与 thinkingBudget 冲突 thinking_config["includeThoughts"] = return_thoughts # 检查最后一个 assistant 消息是否以 thinking 块开始 From 3ba35aca0d19cf220b366e104f2fa28a44cf96c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 17 Jan 2026 08:00:52 +0000 Subject: [PATCH 145/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 05db720d5..6ec1d2e1a 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=a73f272fd3768a051026611b64cd6015fef11c0e -short_hash=a73f272 -message=让claude格式工具调用更好 -date=2026-01-15 23:52:43 +0800 +full_hash=abcd1afb74ad6ec5702fa46547faabf50a63e366 +short_hash=abcd1af +message=Merge pull request #291 from Dongmayyys/fix/thinking-budget-level-conflict +date=2026-01-17 16:00:44 +0800 From f4dad6bbeaca1c760cefa8ffeb1bfb6f3187a174 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Jan 2026 05:07:29 +0000 Subject: [PATCH 146/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 6ec1d2e1a..54d15c120 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=abcd1afb74ad6ec5702fa46547faabf50a63e366 -short_hash=abcd1af -message=Merge pull request #291 from Dongmayyys/fix/thinking-budget-level-conflict -date=2026-01-17 16:00:44 +0800 +full_hash=7f8622f01e80984412050b0d9425b90ac3e0ce0a +short_hash=7f8622f +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-18 12:44:58 +0800 From 0ea47d1aa49da25cd48c1d7c341eb5f7ff2c2f48 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 18 Jan 2026 13:07:36 +0800 Subject: [PATCH 147/211] =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=80=9D=E8=80=83?= =?UTF-8?q?=E5=90=8E=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/gemini_fix.py | 118 +++++++++++++++++++++++++++++------- src/utils.py | 18 +++++- 2 files changed, 111 insertions(+), 25 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 9c6502294..c6010c363 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -65,8 +65,12 @@ def prepare_image_generation_request( def get_base_model_name(model_name: str) -> str: """移除模型名称中的后缀,返回基础模型名""" - # 按照从长到短的顺序排列,避免 -think 先于 -maxthinking 被匹配 - suffixes = ["-maxthinking", "-nothinking", "-search", "-think"] + # 按照从长到短的顺序排列,避免短后缀先于长后缀被匹配 + suffixes = [ + "-maxthinking", "-nothinking", # 兼容旧模式 + "-minimal", "-medium", "-search", "-think", # 中等长度后缀 + "-high", "-max", "-low" # 短后缀 + ] result = model_name changed = True # 持续循环直到没有任何后缀可以移除 @@ -80,25 +84,78 @@ def get_base_model_name(model_name: str) -> str: return result -def get_thinking_settings(model_name: str) -> tuple[Optional[int], bool]: +def get_thinking_settings(model_name: str) -> tuple[Optional[int], Optional[str]]: """ 根据模型名称获取思考配置 + 支持两种模式: + 1. CLI 模式思考预算 (Gemini 2.5 系列): -max, -high, -medium, -low, -minimal + 2. CLI 模式思考等级 (Gemini 3 Preview 系列): -high, -medium, -low, -minimal (仅 3-flash) + 3. 兼容旧模式: -maxthinking, -nothinking (不返回给用户) + Returns: - (thinking_budget, include_thoughts): 思考预算和是否包含思考内容 + (thinking_budget, thinking_level): 思考预算和思考等级 """ base_model = get_base_model_name(model_name) + # ========== 兼容旧模式 (不返回给用户) ========== if "-nothinking" in model_name: - # nothinking 模式: 限制思考,pro模型仍包含thoughts - return 128, "pro" in base_model + # nothinking 模式: 限制思考 + return 128, None elif "-maxthinking" in model_name: # maxthinking 模式: 最大思考预算 budget = 24576 if "flash" in base_model else 32768 - return budget, True - else: - # 默认模式: 不设置thinking budget - return None, True + return budget, None + + # ========== 新 CLI 模式: 基于思考预算/等级 ========== + + # Gemini 3 Preview 系列: 使用 thinkingLevel + if "gemini-3" in base_model: + if "-high" in model_name: + return None, "high" + elif "-medium" in model_name: + # 仅 3-flash-preview 支持 medium + if "flash" in base_model: + return None, "medium" + # pro 系列不支持 medium,返回 Default + return None, None + elif "-low" in model_name: + return None, "low" + elif "-minimal" in model_name: + # 仅 3-flash-preview 支持 minimal,pro 不支持 + if "flash" in base_model: + return None, "minimal" + # pro 系列不支持 minimal,返回 Default + return None, None + else: + # Default: 不设置 thinking 配置 + return None, None + + # Gemini 2.5 系列: 使用 thinkingBudget + elif "gemini-2.5" in base_model: + if "-max" in model_name: + # 2.5-flash-max: 24576, 2.5-pro-max: 32768 + budget = 24576 if "flash" in base_model else 32768 + return budget, None + elif "-high" in model_name: + # 2.5-flash-high: 16000, 2.5-pro-high: 16000 + return 16000, None + elif "-medium" in model_name: + # 2.5-flash-medium: 8192, 2.5-pro-medium: 8192 + return 8192, None + elif "-low" in model_name: + # 2.5-flash-low: 1024, 2.5-pro-low: 1024 + return 1024, None + elif "-minimal" in model_name: + # 2.5-flash-minimal: 0, 2.5-pro-minimal: 128 + budget = 0 if "flash" in base_model else 128 + return budget, None + else: + # Default: 不设置 thinking budget + return None, None + + # 其他模型: 不设置 thinking 配置 + return None, None def is_search_model(model_name: str) -> bool: @@ -150,28 +207,45 @@ async def normalize_gemini_request( # ========== 模式特定处理 ========== if mode == "geminicli": # 1. 思考设置 - # 优先使用 get_thinking_settings 获取的思考预算 - thinking_budget, _ = get_thinking_settings(model) - - # 其次使用传入的思考预算 - if thinking_budget is None: + # 优先使用 get_thinking_settings 获取的思考预算和等级 + thinking_budget, thinking_level = get_thinking_settings(model) + + # 其次使用传入的思考预算(如果未从模型名称获取) + if thinking_budget is None and thinking_level is None: thinking_budget = generation_config.get("thinkingConfig", {}).get("thinkingBudget") - - # 假如 is_thinking_model 为真或者思考预算不为0,设置 thinkingConfig - if is_thinking_model(model) or (thinking_budget and thinking_budget != 0): + thinking_level = generation_config.get("thinkingConfig", {}).get("thinkingLevel") + + # 假如 is_thinking_model 为真或者思考预算/等级不为空,设置 thinkingConfig + if is_thinking_model(model) or thinking_budget is not None or thinking_level is not None: # 确保 thinkingConfig 存在 if "thinkingConfig" not in generation_config: generation_config["thinkingConfig"] = {} thinking_config = generation_config["thinkingConfig"] - # 设置思考预算 - if thinking_budget: + # 设置思考预算或等级(互斥) + if thinking_budget is not None: thinking_config["thinkingBudget"] = thinking_budget thinking_config.pop("thinkingLevel", None) # 避免与 thinkingBudget 冲突 + elif thinking_level is not None: + thinking_config["thinkingLevel"] = thinking_level + thinking_config.pop("thinkingBudget", None) # 避免与 thinkingLevel 冲突 + + # includeThoughts 逻辑: + # 1. 如果是 pro 模型,始终为 True + # 2. 如果不是 pro 模型,检查是否有思考预算或思考等级 + base_model = get_base_model_name(model) + if "pro" in base_model: + include_thoughts = True + else: + # 非 pro 模型: 有思考预算或等级才包含思考 + include_thoughts = thinking_budget is not None or thinking_level is not None + + # 最终使用配置值覆盖(如果配置明确指定) + if return_thoughts is not None: + include_thoughts = return_thoughts - # includeThoughts 使用配置值 - thinking_config["includeThoughts"] = return_thoughts + thinking_config["includeThoughts"] = include_thoughts # 2. 搜索模型添加 Google Search if is_search_model(model): diff --git a/src/utils.py b/src/utils.py index 1ec01f926..c7274b9ce 100644 --- a/src/utils.py +++ b/src/utils.py @@ -109,9 +109,21 @@ def get_available_models(router_type: str = "openai") -> List[str]: # 流式抗截断模型 (仅在流式传输时有效,前缀格式) models.append(f"流式抗截断/{base_model}") - # 支持thinking模式后缀与功能前缀组合 - # 新增: 支持多后缀组合 (thinking + search) - thinking_suffixes = ["-maxthinking", "-nothinking"] + # 定义思考后缀(根据模型系列不同) + thinking_suffixes = [] + + # Gemini 2.5 系列: 使用思考预算后缀 + if "gemini-2.5" in base_model: + thinking_suffixes = ["-max", "-high", "-medium", "-low", "-minimal"] + # Gemini 3 系列: 使用思考等级后缀 + elif "gemini-3" in base_model: + if "flash" in base_model: + # 3-flash-preview: 支持 high/medium/low/minimal + thinking_suffixes = ["-high", "-medium", "-low", "-minimal"] + elif "pro" in base_model: + # 3-pro-preview: 支持 high/low + thinking_suffixes = ["-high", "-low"] + search_suffix = "-search" # 1. 单独的 thinking 后缀 From 2fcdd136209ea9aacf34f2d3cb85881502aa1975 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Jan 2026 05:08:07 +0000 Subject: [PATCH 148/211] chore: update version.txt [skip ci] --- version.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.txt b/version.txt index 54d15c120..dab4e4b56 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=7f8622f01e80984412050b0d9425b90ac3e0ce0a -short_hash=7f8622f +full_hash=a35d3eb5c782f06fbe163f1c257edfcc34b592ae +short_hash=a35d3eb message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-18 12:44:58 +0800 +date=2026-01-18 13:07:54 +0800 From f008b374bacc1e6c9a4fd7446b22f1a5ca394e21 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 18 Jan 2026 13:15:24 +0800 Subject: [PATCH 149/211] Update openai2gemini.py --- src/converter/openai2gemini.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 5531ad31c..558cd9099 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -403,15 +403,15 @@ def _clean_schema_for_gemini(schema: Any, root_schema: Optional[Dict[str, Any]] result = dict(schema) # 3. 类型映射(转换为大写) + # 注意:Gemini API 的 type 字段必须是字符串,不能是数组 if "type" in result: type_value = result["type"] - - # 处理 type: ["string", "null"] 的情况 + + # 如果 type 是列表,提取主要类型(非 null) if isinstance(type_value, list): primary_type = next((t for t in type_value if t != "null"), None) - if primary_type: - type_value = primary_type - + type_value = primary_type if primary_type else "STRING" # 默认为 STRING + # 类型映射 type_map = { "string": "STRING", @@ -421,9 +421,13 @@ def _clean_schema_for_gemini(schema: Any, root_schema: Optional[Dict[str, Any]] "array": "ARRAY", "object": "OBJECT", } - + if isinstance(type_value, str) and type_value.lower() in type_map: + # 确保 result["type"] 是字符串而不是列表 result["type"] = type_map[type_value.lower()] + else: + # 未知类型,删除该字段 + del result["type"] # 4. 处理 ARRAY 的 items if result.get("type") == "ARRAY": From 50e970ef7a622225d16abe7fba1a99cd95adbf05 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Jan 2026 05:15:35 +0000 Subject: [PATCH 150/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index dab4e4b56..ef8aaf4d2 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=a35d3eb5c782f06fbe163f1c257edfcc34b592ae -short_hash=a35d3eb -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-18 13:07:54 +0800 +full_hash=f008b374bacc1e6c9a4fd7446b22f1a5ca394e21 +short_hash=f008b37 +message=Update openai2gemini.py +date=2026-01-18 13:15:24 +0800 From 90479e0170e5620a6b3c94d176cc0868dee642d4 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 18 Jan 2026 13:32:55 +0800 Subject: [PATCH 151/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index c6010c363..269490cb8 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -101,6 +101,8 @@ def get_thinking_settings(model_name: str) -> tuple[Optional[int], Optional[str] # ========== 兼容旧模式 (不返回给用户) ========== if "-nothinking" in model_name: # nothinking 模式: 限制思考 + if "flash" in base_model: + return 0, None return 128, None elif "-maxthinking" in model_name: # maxthinking 模式: 最大思考预算 @@ -122,10 +124,6 @@ def get_thinking_settings(model_name: str) -> tuple[Optional[int], Optional[str] elif "-low" in model_name: return None, "low" elif "-minimal" in model_name: - # 仅 3-flash-preview 支持 minimal,pro 不支持 - if "flash" in base_model: - return None, "minimal" - # pro 系列不支持 minimal,返回 Default return None, None else: # Default: 不设置 thinking 配置 From 76e7034aedd7da0c4fedfcb0a3fc75c7ceb3e928 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 18 Jan 2026 13:38:07 +0800 Subject: [PATCH 152/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 269490cb8..64b004ef0 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -4,6 +4,7 @@ ──────────────────────────────────────────────────────────────── """ +from tkinter import N from typing import Any, Dict, Optional from log import log @@ -237,7 +238,11 @@ async def normalize_gemini_request( include_thoughts = True else: # 非 pro 模型: 有思考预算或等级才包含思考 - include_thoughts = thinking_budget is not None or thinking_level is not None + # 注意: 思考预算为 0 时不包含思考 + if (thinking_budget is not None and thinking_budget > 0) or thinking_level is not None: + include_thoughts = True + else: + include_thoughts = None # 最终使用配置值覆盖(如果配置明确指定) if return_thoughts is not None: From 7b7eaca01dc7fa60e554bcb2498a64db878e04a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Jan 2026 05:38:14 +0000 Subject: [PATCH 153/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index ef8aaf4d2..46393d17d 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=f008b374bacc1e6c9a4fd7446b22f1a5ca394e21 -short_hash=f008b37 -message=Update openai2gemini.py -date=2026-01-18 13:15:24 +0800 +full_hash=76e7034aedd7da0c4fedfcb0a3fc75c7ceb3e928 +short_hash=76e7034 +message=Update gemini_fix.py +date=2026-01-18 13:38:07 +0800 From 85457f40a1343217f80534aa8eac7ee2cc27bc3b Mon Sep 17 00:00:00 2001 From: sukaka <101160581+su-kaka@users.noreply.github.com> Date: Sun, 18 Jan 2026 15:31:30 +0800 Subject: [PATCH 154/211] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20gemini=5Ffix.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/gemini_fix.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 64b004ef0..ba252c2d4 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -3,8 +3,6 @@ 提供对 Gemini API 请求体和响应的标准化处理 ──────────────────────────────────────────────────────────────── """ - -from tkinter import N from typing import Any, Dict, Optional from log import log From 3aa3c94510dd7e9c4b1d5329e0e42ae8dd20a7df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 18 Jan 2026 07:31:38 +0000 Subject: [PATCH 155/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 46393d17d..00d0baf56 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=76e7034aedd7da0c4fedfcb0a3fc75c7ceb3e928 -short_hash=76e7034 -message=Update gemini_fix.py -date=2026-01-18 13:38:07 +0800 +full_hash=85457f40a1343217f80534aa8eac7ee2cc27bc3b +short_hash=85457f4 +message=更新 gemini_fix.py +date=2026-01-18 15:31:30 +0800 From 1e653dca3a35ab53cd691420af03fcf37f8fc1e4 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 20 Jan 2026 22:38:41 +0800 Subject: [PATCH 156/211] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32e381fff..d9bc4012d 100644 --- a/README.md +++ b/README.md @@ -319,7 +319,7 @@ ghcr.io/su-kaka/gcli2api:latest ## ⚠️ 注意事项 -- 当前 OAuth 验证流程**仅支持本地主机(localhost)访问**,即须通过 `http://127.0.0.1:7861/auth` 完成认证(默认端口 7861,可通过 PORT 环境变量修改)。 +- 当前 OAuth 验证流程**仅支持本地主机(localhost)访问**,即须通过 `http://127.0.0.1:7861` 完成认证(默认端口 7861,可通过 PORT 环境变量修改)。 - **如需在云服务器或其他远程环境部署,请先在本地运行服务并完成 OAuth 验证,获得生成的 json 凭证文件(位于 `./geminicli/creds` 目录)后,再在auth面板将该文件上传即可。** - **请严格遵守使用限制,仅用于个人学习和非商业用途** @@ -327,7 +327,7 @@ ghcr.io/su-kaka/gcli2api:latest ## 配置说明 -1. 访问 `http://127.0.0.1:7861/auth` (默认端口,可通过 PORT 环境变量修改) +1. 访问 `http://127.0.0.1:7861` (默认端口,可通过 PORT 环境变量修改) 2. 完成 OAuth 认证流程(默认密码:`pwd`,可通过环境变量修改) - **GCLI 模式**:用于获取 Google Cloud Gemini API 凭证 - **Antigravity 模式**:用于获取 Google Antigravity API 凭证 From 3ea52f675e075bd10639bc9a98d38ab3a19680c2 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 20 Jan 2026 22:39:23 +0800 Subject: [PATCH 157/211] Update README_EN.md --- docs/README_EN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README_EN.md b/docs/README_EN.md index 413fda2b9..88495ce00 100644 --- a/docs/README_EN.md +++ b/docs/README_EN.md @@ -282,7 +282,7 @@ docker run -d --name gcli2api --network host -e API_PASSWORD=api_pwd -e PANEL_PA ## ⚠️ Important Notes -- The current OAuth authentication process **only supports localhost access**, meaning authentication must be completed through `http://127.0.0.1:7861/auth` (default port 7861, modifiable via PORT environment variable). +- The current OAuth authentication process **only supports localhost access**, meaning authentication must be completed through `http://127.0.0.1:7861/` (default port 7861, modifiable via PORT environment variable). - **For deployment on cloud servers or other remote environments, please first run the service locally and complete OAuth authentication to obtain the generated json credential files (located in the `./geminicli/creds` directory), then upload these files via the auth panel.** - **Please strictly comply with usage restrictions, only for personal learning and non-commercial purposes** @@ -290,7 +290,7 @@ docker run -d --name gcli2api --network host -e API_PASSWORD=api_pwd -e PANEL_PA ## Configuration Instructions -1. Visit `http://127.0.0.1:7861/auth` (default port, modifiable via PORT environment variable) +1. Visit `http://127.0.0.1:7861/` (default port, modifiable via PORT environment variable) 2. Complete OAuth authentication flow (default password: `pwd`, modifiable via environment variables) - **GCLI Mode**: For obtaining Google Cloud Gemini API credentials - **Antigravity Mode**: For obtaining Google Antigravity API credentials From fcea29a3a1e76f13e2a8cdf4011a9fff2540418d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 20 Jan 2026 14:39:38 +0000 Subject: [PATCH 158/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 00d0baf56..bf7c7164a 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=85457f40a1343217f80534aa8eac7ee2cc27bc3b -short_hash=85457f4 -message=更新 gemini_fix.py -date=2026-01-18 15:31:30 +0800 +full_hash=cfebe20eb4c8055d14a1093afdbb64f9eca2d622 +short_hash=cfebe20 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-20 22:39:27 +0800 From ea0b99ac08293d8eb9a87f27c7113a717a0f7a4d Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 20 Jan 2026 22:48:41 +0800 Subject: [PATCH 159/211] Update web.py --- web.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web.py b/web.py index d1b348238..004d01c4c 100644 --- a/web.py +++ b/web.py @@ -21,8 +21,8 @@ from src.router.antigravity.gemini import router as antigravity_gemini_router from src.router.antigravity.anthropic import router as antigravity_anthropic_router from src.router.antigravity.model_list import router as antigravity_model_list_router -from src.router.geminicli.openai import router as openai_router -from src.router.geminicli.gemini import router as gemini_router +from src.router.geminicli.openai import router as geminicli_openai_router +from src.router.geminicli.gemini import router as geminicli_gemini_router from src.router.geminicli.anthropic import router as geminicli_anthropic_router from src.router.geminicli.model_list import router as geminicli_model_list_router from src.task_manager import shutdown_all_tasks @@ -101,10 +101,10 @@ async def lifespan(app: FastAPI): # 挂载路由器 # OpenAI兼容路由 - 处理OpenAI格式请求 -app.include_router(openai_router, prefix="", tags=["OpenAI Compatible API"]) +app.include_router(geminicli_openai_router, prefix="", tags=["Geminicli OpenAI API"]) # Gemini原生路由 - 处理Gemini格式请求 -app.include_router(gemini_router, prefix="", tags=["Gemini Native API"]) +app.include_router(geminicli_gemini_router, prefix="", tags=["Geminicli Gemini API"]) # Geminicli模型列表路由 - 处理Gemini格式的模型列表请求 app.include_router(geminicli_model_list_router, prefix="", tags=["Geminicli Model List"]) From 6672760383fce983b9bfe70300603602c1f6e44b Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 20 Jan 2026 22:49:01 +0800 Subject: [PATCH 160/211] Update web.py --- web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web.py b/web.py index 004d01c4c..0909dd6cb 100644 --- a/web.py +++ b/web.py @@ -127,7 +127,7 @@ async def lifespan(app: FastAPI): # Web路由 - 包含认证、凭证管理和控制面板功能 app.include_router(web_router, prefix="", tags=["Web Interface"]) -# 静态文件路由 - 服务docs目录下的文件(如捐赠图片) +# 静态文件路由 - 服务docs目录下的文件 app.mount("/docs", StaticFiles(directory="docs"), name="docs") # 静态文件路由 - 服务front目录下的文件(HTML、JS、CSS等) From 1b36300cc304c6a3c3378ec7f79add563efe9f17 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Thu, 22 Jan 2026 22:02:53 +0800 Subject: [PATCH 161/211] Update geminicli.py --- src/api/geminicli.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/api/geminicli.py b/src/api/geminicli.py index 108ce6755..9cfe762aa 100644 --- a/src/api/geminicli.py +++ b/src/api/geminicli.py @@ -262,25 +262,7 @@ async def refresh_credential_fast(): if success_recorded: log.debug(f"[GEMINICLI STREAM] 流式响应完成,模型: {model_name}") return - elif not need_retry: - # 没有收到任何数据(空回复),需要重试 - log.warning(f"[GEMINICLI STREAM] 收到空回复,无任何内容,凭证: {current_file}") - await record_api_call_error( - credential_manager, current_file, 200, - None, mode="geminicli", model_key=model_group - ) - - if attempt < max_retries: - need_retry = True - else: - log.error(f"[GEMINICLI STREAM] 空回复达到最大重试次数") - yield Response( - content=json.dumps({"error": "服务返回空回复"}), - status_code=500, - media_type="application/json" - ) - return - + # 统一处理重试 if need_retry: log.info(f"[GEMINICLI STREAM] 重试请求 (attempt {attempt + 2}/{max_retries + 1})...") From 0301375783c15dacc7b2f7cf9ea428a5a3f74527 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 22 Jan 2026 14:03:41 +0000 Subject: [PATCH 162/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index bf7c7164a..b79298f26 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=cfebe20eb4c8055d14a1093afdbb64f9eca2d622 -short_hash=cfebe20 -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-20 22:39:27 +0800 +full_hash=1b36300cc304c6a3c3378ec7f79add563efe9f17 +short_hash=1b36300 +message=Update geminicli.py +date=2026-01-22 22:02:53 +0800 From 9e471ee3c448f1c7562a0dc0e339d0ad6e8dfdce Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Fri, 23 Jan 2026 14:02:28 +0800 Subject: [PATCH 163/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index ba252c2d4..8fc1ee21f 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -229,22 +229,18 @@ async def normalize_gemini_request( thinking_config.pop("thinkingBudget", None) # 避免与 thinkingLevel 冲突 # includeThoughts 逻辑: - # 1. 如果是 pro 模型,始终为 True + # 1. 如果是 pro 模型,为 return_thoughts # 2. 如果不是 pro 模型,检查是否有思考预算或思考等级 base_model = get_base_model_name(model) if "pro" in base_model: - include_thoughts = True + include_thoughts = return_thoughts else: # 非 pro 模型: 有思考预算或等级才包含思考 # 注意: 思考预算为 0 时不包含思考 - if (thinking_budget is not None and thinking_budget > 0) or thinking_level is not None: - include_thoughts = True + if thinking_budget is None or thinking_budget == 0: + include_thoughts = False else: - include_thoughts = None - - # 最终使用配置值覆盖(如果配置明确指定) - if return_thoughts is not None: - include_thoughts = return_thoughts + include_thoughts = return_thoughts thinking_config["includeThoughts"] = include_thoughts From 0f8f4a92a1c69d8470b2b4b69675e153af773534 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Jan 2026 06:02:33 +0000 Subject: [PATCH 164/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index b79298f26..9afefe683 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=1b36300cc304c6a3c3378ec7f79add563efe9f17 -short_hash=1b36300 -message=Update geminicli.py -date=2026-01-22 22:02:53 +0800 +full_hash=9e471ee3c448f1c7562a0dc0e339d0ad6e8dfdce +short_hash=9e471ee +message=Update gemini_fix.py +date=2026-01-23 14:02:28 +0800 From 71a0457b7b4236f232f34a0956d87cdd435d6303 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 24 Jan 2026 14:29:19 +0800 Subject: [PATCH 165/211] =?UTF-8?q?503=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 10 +++++----- src/api/geminicli.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 623692a93..88bde5b50 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -186,8 +186,8 @@ async def refresh_credential_fast(): except Exception: error_body = "" - # 如果错误码是429或者在禁用码当中,做好记录后进行重试 - if status_code == 429 or status_code in DISABLE_ERROR_CODES: + # 如果错误码是429、503或者在禁用码当中,做好记录后进行重试 + if status_code == 429 or status_code == 503 or status_code in DISABLE_ERROR_CODES: log.warning(f"[ANTIGRAVITY STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500] if error_body else '无'}") # 并行预热下一个凭证,不阻塞当前处理 @@ -200,7 +200,7 @@ async def refresh_credential_fast(): # 记录错误 cooldown_until = None - if status_code == 429 and error_body: + if status_code == 429 or status_code == 503 and error_body: # 使用已缓存的error_body解析冷却时间 try: cooldown_until = await parse_and_log_cooldown(error_body, mode="antigravity") @@ -487,7 +487,7 @@ async def refresh_credential_fast(): except Exception: pass - if status_code == 429 or status_code in DISABLE_ERROR_CODES: + if status_code == 429 or status_code == 503 or status_code in DISABLE_ERROR_CODES: log.warning(f"[ANTIGRAVITY] 非流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_text[:500] if error_text else '无'}") # 并行预热下一个凭证,不阻塞当前处理 @@ -500,7 +500,7 @@ async def refresh_credential_fast(): # 记录错误 cooldown_until = None - if status_code == 429 and error_text: + if status_code == 429 or status_code == 503 and error_text: # 使用已缓存的error_text解析冷却时间 try: cooldown_until = await parse_and_log_cooldown(error_text, mode="antigravity") diff --git a/src/api/geminicli.py b/src/api/geminicli.py index 9cfe762aa..37fe2c6f2 100644 --- a/src/api/geminicli.py +++ b/src/api/geminicli.py @@ -196,8 +196,8 @@ async def refresh_credential_fast(): except Exception: error_body = "" - # 如果错误码是429或者在禁用码当中,做好记录后进行重试 - if status_code == 429 or status_code in DISABLE_ERROR_CODES: + # 如果错误码是429、503或者在禁用码当中,做好记录后进行重试 + if status_code == 429 or status_code == 503 or status_code in DISABLE_ERROR_CODES: log.warning(f"[GEMINICLI STREAM] 流式请求失败 (status={status_code}), 凭证: {current_file}, 响应: {error_body[:500] if error_body else '无'}") # 并行预热下一个凭证,不阻塞当前处理 @@ -210,7 +210,7 @@ async def refresh_credential_fast(): # 记录错误 cooldown_until = None - if status_code == 429 and error_body: + if status_code == 429 or status_code == 503 and error_body: # 使用已缓存的error_body解析冷却时间 try: cooldown_until = await parse_and_log_cooldown(error_body, mode="geminicli") @@ -514,7 +514,7 @@ async def refresh_credential_fast(): # 记录错误 cooldown_until = None - if status_code == 429 and error_text: + if status_code == 429 or status_code == 503 and error_text: # 使用已缓存的error_text解析冷却时间 try: cooldown_until = await parse_and_log_cooldown(error_text, mode="geminicli") From a2411a1c713917fb7b30c3b8dafff2ec8b7cfe80 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Jan 2026 06:29:22 +0000 Subject: [PATCH 166/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 9afefe683..3430b20e4 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=9e471ee3c448f1c7562a0dc0e339d0ad6e8dfdce -short_hash=9e471ee -message=Update gemini_fix.py -date=2026-01-23 14:02:28 +0800 +full_hash=71a0457b7b4236f232f34a0956d87cdd435d6303 +short_hash=71a0457 +message=503重试 +date=2026-01-24 14:29:19 +0800 From d4ec2ae9a7261b153cc18d7f4ee561a7053835a0 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 24 Jan 2026 14:34:05 +0800 Subject: [PATCH 167/211] Update gemini_fix.py --- src/converter/gemini_fix.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/converter/gemini_fix.py b/src/converter/gemini_fix.py index 8fc1ee21f..6ec3a65eb 100644 --- a/src/converter/gemini_fix.py +++ b/src/converter/gemini_fix.py @@ -234,6 +234,11 @@ async def normalize_gemini_request( base_model = get_base_model_name(model) if "pro" in base_model: include_thoughts = return_thoughts + elif "3-flash" in base_model: + if thinking_level is None: + include_thoughts = False + else: + include_thoughts = return_thoughts else: # 非 pro 模型: 有思考预算或等级才包含思考 # 注意: 思考预算为 0 时不包含思考 From c92c8343df40e671a08b23cf7ef2602fb8e96f86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Jan 2026 06:34:08 +0000 Subject: [PATCH 168/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 3430b20e4..1d03124bb 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=71a0457b7b4236f232f34a0956d87cdd435d6303 -short_hash=71a0457 -message=503重试 -date=2026-01-24 14:29:19 +0800 +full_hash=d4ec2ae9a7261b153cc18d7f4ee561a7053835a0 +short_hash=d4ec2ae +message=Update gemini_fix.py +date=2026-01-24 14:34:05 +0800 From d6d1fdeb195adc1c203468c1b8876e7660e47f89 Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sun, 25 Jan 2026 20:55:41 +0800 Subject: [PATCH 169/211] =?UTF-8?q?503=E9=87=8D=E8=AF=95=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/antigravity.py | 2 +- src/api/geminicli.py | 2 +- src/api/utils.py | 13 +++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 88bde5b50..5ac07f421 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -200,7 +200,7 @@ async def refresh_credential_fast(): # 记录错误 cooldown_until = None - if status_code == 429 or status_code == 503 and error_body: + if (status_code == 429 or status_code == 503) and error_body: # 使用已缓存的error_body解析冷却时间 try: cooldown_until = await parse_and_log_cooldown(error_body, mode="antigravity") diff --git a/src/api/geminicli.py b/src/api/geminicli.py index 37fe2c6f2..d569c3781 100644 --- a/src/api/geminicli.py +++ b/src/api/geminicli.py @@ -210,7 +210,7 @@ async def refresh_credential_fast(): # 记录错误 cooldown_until = None - if status_code == 429 or status_code == 503 and error_body: + if (status_code == 429 or status_code == 503) and error_body: # 使用已缓存的error_body解析冷却时间 try: cooldown_until = await parse_and_log_cooldown(error_body, mode="geminicli") diff --git a/src/api/utils.py b/src/api/utils.py index 491ac2537..753557f09 100644 --- a/src/api/utils.py +++ b/src/api/utils.py @@ -75,11 +75,12 @@ async def handle_error_with_retry( ) -> bool: """ 统一处理错误和重试逻辑 - + 仅在以下情况下进行自动重试: 1. 429错误(速率限制) - 2. 导致凭证封禁的错误(AUTO_BAN_ERROR_CODES配置) - + 2. 503错误(服务不可用) + 3. 导致凭证封禁的错误(AUTO_BAN_ERROR_CODES配置) + Args: credential_manager: 凭证管理器实例 status_code: HTTP状态码 @@ -110,10 +111,10 @@ async def handle_error_with_retry( return True return False - # 如果不触发自动封禁,仅对429错误进行重试 - if status_code == 429 and retry_enabled and attempt < max_retries: + # 如果不触发自动封禁,仅对429和503错误进行重试 + if (status_code == 429 or status_code == 503) and retry_enabled and attempt < max_retries: log.info( - f"[{mode.upper()} RETRY] 429 rate limit encountered, retrying " + f"[{mode.upper()} RETRY] {status_code} error encountered, retrying " f"(attempt {attempt + 1}/{max_retries})" ) await asyncio.sleep(retry_interval) From aa2c7cfcb9527a03c5342f996fccf365c4c0a87d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 25 Jan 2026 12:55:50 +0000 Subject: [PATCH 170/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index 1d03124bb..b04090c91 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=d4ec2ae9a7261b153cc18d7f4ee561a7053835a0 -short_hash=d4ec2ae -message=Update gemini_fix.py -date=2026-01-24 14:34:05 +0800 +full_hash=b437ce3072af33a2a867213d1c4bb04bd5137209 +short_hash=b437ce3 +message=Merge branch 'master' of https://github.com/su-kaka/gcli2api +date=2026-01-25 20:55:44 +0800 From 0a641c48967cf45fe45b8b4734cdd0c52cfdb10e Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Tue, 27 Jan 2026 19:48:20 +0800 Subject: [PATCH 171/211] Update mongodb_manager.py --- src/storage/mongodb_manager.py | 57 +++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/src/storage/mongodb_manager.py b/src/storage/mongodb_manager.py index 1184a2f2f..f9741e42e 100644 --- a/src/storage/mongodb_manager.py +++ b/src/storage/mongodb_manager.py @@ -24,6 +24,19 @@ class MongoDBManager: "model_cooldowns", } + @staticmethod + def _escape_model_key(model_key: str) -> str: + """ + 转义模型键中的点号,避免 MongoDB 将其解释为嵌套结构 + + Args: + model_key: 原始模型键 (如 "gemini-2.5-flash") + + Returns: + 转义后的模型键 (如 "gemini-2-5-flash") + """ + return model_key.replace(".", "-") + def __init__(self): self._client: Optional[AsyncIOMotorClient] = None self._db: Optional[AsyncIOMotorDatabase] = None @@ -170,6 +183,8 @@ async def get_next_available_credential( # 如果提供了 model_key,添加冷却检查 if model_key: + # 转义模型键中的点号 + escaped_model_key = self._escape_model_key(model_key) pipeline.extend([ # 第二步: 添加冷却状态字段 { @@ -177,9 +192,9 @@ async def get_next_available_credential( "is_available": { "$or": [ # model_cooldowns 中没有该 model_key - {"$not": {"$ifNull": [f"$model_cooldowns.{model_key}", False]}}, + {"$not": {"$ifNull": [f"$model_cooldowns.{escaped_model_key}", False]}}, # 或者冷却时间已过期 - {"$lte": [f"$model_cooldowns.{model_key}", current_time]} + {"$lte": [f"$model_cooldowns.{escaped_model_key}", current_time]} ] } } @@ -526,17 +541,26 @@ async def get_credential_state(self, filename: str, mode: str = "geminicli") -> try: collection_name = self._get_collection_name(mode) collection = self._db[collection_name] + current_time = time.time() # 首先尝试精确匹配 doc = await collection.find_one({"filename": filename}) if doc: + model_cooldowns = doc.get("model_cooldowns", {}) + # 过滤掉损坏的数据(dict类型)和过期的冷却 + if model_cooldowns: + model_cooldowns = { + k: v for k, v in model_cooldowns.items() + if isinstance(v, (int, float)) and v > current_time + } + return { "disabled": doc.get("disabled", False), "error_codes": doc.get("error_codes", []), - "last_success": doc.get("last_success", time.time()), + "last_success": doc.get("last_success", current_time), "user_email": doc.get("user_email"), - "model_cooldowns": doc.get("model_cooldowns", {}), + "model_cooldowns": model_cooldowns, } # 如果精确匹配失败,尝试basename匹配 @@ -546,19 +570,27 @@ async def get_credential_state(self, filename: str, mode: str = "geminicli") -> }) if doc: + model_cooldowns = doc.get("model_cooldowns", {}) + # 过滤掉损坏的数据(dict类型)和过期的冷却 + if model_cooldowns: + model_cooldowns = { + k: v for k, v in model_cooldowns.items() + if isinstance(v, (int, float)) and v > current_time + } + return { "disabled": doc.get("disabled", False), "error_codes": doc.get("error_codes", []), - "last_success": doc.get("last_success", time.time()), + "last_success": doc.get("last_success", current_time), "user_email": doc.get("user_email"), - "model_cooldowns": doc.get("model_cooldowns", {}), + "model_cooldowns": model_cooldowns, } # 返回默认状态 return { "disabled": False, "error_codes": [], - "last_success": time.time(), + "last_success": current_time, "user_email": None, "model_cooldowns": {}, } @@ -600,7 +632,7 @@ async def get_all_credential_states(self, mode: str = "geminicli") -> Dict[str, if model_cooldowns: model_cooldowns = { k: v for k, v in model_cooldowns.items() - if v > current_time + if isinstance(v, (int, float)) and v > current_time } states[filename] = { @@ -710,7 +742,7 @@ async def get_credentials_summary( if model_cooldowns: active_cooldowns = { k: v for k, v in model_cooldowns.items() - if v > current_time + if isinstance(v, (int, float)) and v > current_time } summary = { @@ -843,13 +875,16 @@ async def set_model_cooldown( collection_name = self._get_collection_name(mode) collection = self._db[collection_name] + # 转义模型键中的点号 + escaped_model_key = self._escape_model_key(model_key) + # 使用原子操作直接更新,避免竞态条件 if cooldown_until is None: # 删除指定模型的冷却 result = await collection.update_one( {"filename": filename}, { - "$unset": {f"model_cooldowns.{model_key}": ""}, + "$unset": {f"model_cooldowns.{escaped_model_key}": ""}, "$set": {"updated_at": time.time()} } ) @@ -859,7 +894,7 @@ async def set_model_cooldown( {"filename": filename}, { "$set": { - f"model_cooldowns.{model_key}": cooldown_until, + f"model_cooldowns.{escaped_model_key}": cooldown_until, "updated_at": time.time() } } From fd869d5d89cca6deeb3c52f111c676d4b02a8584 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 27 Jan 2026 11:48:32 +0000 Subject: [PATCH 172/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index b04090c91..d686dde06 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=b437ce3072af33a2a867213d1c4bb04bd5137209 -short_hash=b437ce3 -message=Merge branch 'master' of https://github.com/su-kaka/gcli2api -date=2026-01-25 20:55:44 +0800 +full_hash=0a641c48967cf45fe45b8b4734cdd0c52cfdb10e +short_hash=0a641c4 +message=Update mongodb_manager.py +date=2026-01-27 19:48:20 +0800 From 8bdafebe734358926a6ab09a91bcc3f2c4324fd3 Mon Sep 17 00:00:00 2001 From: lei <409239349@qq.com> Date: Fri, 30 Jan 2026 04:20:43 +0800 Subject: [PATCH 173/211] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E5=B9=B6=E5=8F=91=E8=B0=83=E7=94=A8=E6=97=B6?= =?UTF-8?q?=20Gemini=20API=20=E6=8A=A5=20function=20response=20parts=20mis?= =?UTF-8?q?match=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/converter/openai2gemini.py | 38 ++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/converter/openai2gemini.py b/src/converter/openai2gemini.py index 558cd9099..fbcb57785 100644 --- a/src/converter/openai2gemini.py +++ b/src/converter/openai2gemini.py @@ -964,11 +964,24 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di if func_name: tool_schemas[func_name] = function.get("parameters", {}) + # 用于累积连续的 tool message 的 functionResponse parts + pending_tool_parts = [] + + def flush_pending_tool_parts(): + """将累积的 tool parts 作为单个 contents 条目追加""" + nonlocal pending_tool_parts + if pending_tool_parts: + contents.append({ + "role": "user", + "parts": pending_tool_parts + }) + pending_tool_parts = [] + for message in messages: role = message.get("role", "user") content = message.get("content", "") - # 处理工具消息(tool role) + # 处理工具消息(tool role)- 累积到 pending_tool_parts if role == "tool": tool_call_id = message.get("tool_call_id", "") func_name = message.get("name") @@ -1006,19 +1019,19 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di if not isinstance(response_data, dict): response_data = {"result": response_data} - # 使用原始 ID(不带签名) - contents.append({ - "role": "user", - "parts": [{ - "functionResponse": { - "id": original_id, - "name": func_name, - "response": response_data - } - }] + # 累积 functionResponse part(不立即追加到 contents) + pending_tool_parts.append({ + "functionResponse": { + "id": original_id, + "name": func_name, + "response": response_data + } }) continue + # 遇到非 tool 消息时,先 flush 累积的 tool parts + flush_pending_tool_parts() + # system 消息已经由 merge_system_messages 处理,这里跳过 if role == "system": continue @@ -1104,6 +1117,9 @@ async def convert_openai_to_gemini_request(openai_request: Dict[str, Any]) -> Di elif content: contents.append({"role": role, "parts": [{"text": content}]}) + # 循环结束后,flush 剩余的 tool parts(如果消息列表以 tool 消息结尾) + flush_pending_tool_parts() + # 构建生成配置 generation_config = {} model = openai_request.get("model", "") From 0b0e0ee044304da2549f67f3d19bc5ad4c74be4f Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Fri, 30 Jan 2026 08:15:57 +0800 Subject: [PATCH 174/211] Update utils.py --- src/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.py b/src/utils.py index c7274b9ce..fc97424f7 100644 --- a/src/utils.py +++ b/src/utils.py @@ -13,7 +13,7 @@ GEMINICLI_USER_AGENT = "GeminiCLI/0.1.5 (Windows; AMD64)" -ANTIGRAVITY_USER_AGENT = "antigravity/1.11.3 windows/amd64" +ANTIGRAVITY_USER_AGENT = "antigravity/1.15.8 (Windows; AMD64)" # OAuth Configuration - 标准模式 CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com" From 229d8bc6fa2c3a3a8af40d102da4960d350101cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 30 Jan 2026 00:16:09 +0000 Subject: [PATCH 175/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index d686dde06..cfb072907 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=0a641c48967cf45fe45b8b4734cdd0c52cfdb10e -short_hash=0a641c4 -message=Update mongodb_manager.py -date=2026-01-27 19:48:20 +0800 +full_hash=0b0e0ee044304da2549f67f3d19bc5ad4c74be4f +short_hash=0b0e0ee +message=Update utils.py +date=2026-01-30 08:15:57 +0800 From 9b971c6568dd137cc6c889d8c29a231f39aa7045 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 30 Jan 2026 00:42:52 +0000 Subject: [PATCH 176/211] chore: update version.txt [skip ci] --- version.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/version.txt b/version.txt index cfb072907..d1f58786d 100644 --- a/version.txt +++ b/version.txt @@ -1,4 +1,4 @@ -full_hash=0b0e0ee044304da2549f67f3d19bc5ad4c74be4f -short_hash=0b0e0ee -message=Update utils.py -date=2026-01-30 08:15:57 +0800 +full_hash=5aa2d957694a0ac7a3477d423c670c3fa3804e42 +short_hash=5aa2d95 +message=Merge pull request #304 from leik1000/master +date=2026-01-30 08:42:43 +0800 From 45f43f0e2cf7440ab8fdb22c68608e957442c03b Mon Sep 17 00:00:00 2001 From: su-kaka <3493227712@qq.com> Date: Sat, 31 Jan 2026 10:24:26 +0800 Subject: [PATCH 177/211] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=92=8C=E6=9F=A5=E7=9C=8B=E9=94=99=E8=AF=AF=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/common.js | 126 +++++++++++++++++++++++++++++++++ src/api/antigravity.py | 18 +++-- src/api/geminicli.py | 84 ++++++---------------- src/api/utils.py | 9 ++- src/credential_manager.py | 30 ++++---- src/storage/mongodb_manager.py | 68 +++++++++++++++++- src/storage/sqlite_manager.py | 77 ++++++++++++++++++-- src/web_routes.py | 44 ++++++++++++ 8 files changed, 361 insertions(+), 95 deletions(-) diff --git a/front/common.js b/front/common.js index b07b90313..7cbe9ef4b 100644 --- a/front/common.js +++ b/front/common.js @@ -615,6 +615,7 @@ function createCredCard(credInfo, manager) { ${managerType === 'antigravity' ? `` : ''} + `; @@ -640,6 +641,9 @@ function createCredCard(credInfo, manager) {
点击"查看内容"按钮加载文件详情...
+
+
点击"查看报错"按钮加载报错信息...
+
${managerType === 'antigravity' ? `