diff --git a/src/api/antigravity.py b/src/api/antigravity.py index 73e03c267..29fe05ac6 100644 --- a/src/api/antigravity.py +++ b/src/api/antigravity.py @@ -18,6 +18,7 @@ get_auto_ban_error_codes, get_request_log_enabled, get_request_log_error_codes, + get_return_thoughts_to_frontend, ) from log import log @@ -28,6 +29,7 @@ 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 +from src.thought_filter import filter_sse_chunk, filter_response_bytes # 导入共同的基础功能 from src.api.utils import ( @@ -316,6 +318,7 @@ async def stream_request( Response对象(错误时)或 bytes流/str流(成功时) """ model_name = body.get("model", "") + return_thoughts = await get_return_thoughts_to_frontend() # 请求日志:开始时间与基础条目(成功/失败均会记录一次) log_entry = _base_log_entry(model_name, "", True, log_ctx) @@ -543,6 +546,9 @@ async def _log_stream_result(status_code: int, success: bool, error_message: str else: log.debug(f"[ANTIGRAVITY STREAM RAW] chunk(str): {chunk}") + # 开关关闭时剔除思考片段(后端不遵守 includeThoughts) + if not return_thoughts: + chunk = filter_sse_chunk(chunk) yield chunk # 流式请求完成,检查结果 @@ -654,6 +660,7 @@ async def non_stream_request( Response对象 """ model_name = body.get("model", "") + return_thoughts = await get_return_thoughts_to_frontend() # 请求日志:开始时间与基础条目(成功/失败均会记录一次) log_entry = _base_log_entry(model_name, "", False, log_ctx) @@ -818,8 +825,12 @@ def apply_cred_result(cred_result: Tuple[str, Dict[str, Any]]) -> bool: # 正常响应 await record_api_call_success(current_file, model_name=model_name) await _log_result(200, True, response_content=response.content) + content = response.content + # 开关关闭时剔除思考片段(后端不遵守 includeThoughts) + if not return_thoughts: + content = filter_response_bytes(content) return Response( - content=response.content, + content=content, status_code=200, headers=dict(response.headers) ) diff --git a/src/converter/antigravity_fix.py b/src/converter/antigravity_fix.py index 0d92a04ed..9c4938deb 100644 --- a/src/converter/antigravity_fix.py +++ b/src/converter/antigravity_fix.py @@ -656,16 +656,20 @@ def is_thinking_model(model_name: str) -> bool: def _apply_thinking(profile: ModelProfile, generation_config: Dict[str, Any], return_thoughts: bool) -> None: - """按模型 profile 写入 thinkingConfig (就地修改 generation_config,thinkingConfig 先复制)""" - if profile.thinking == "none": + """按模型 profile 写入 thinkingConfig (就地修改 generation_config,thinkingConfig 先复制)。 + thinkingBudget/thinkingLevel 决定思考深度;includeThoughts 决定是否回传思考内容,两者相互独立。""" + existing = generation_config.get("thinkingConfig") + # 无思考配置的模型不注入 thinkingConfig,避免影响不支持思考的模型 + if profile.thinking == "none" and not existing: return - thinking_config = dict(generation_config.get("thinkingConfig") or {}) - thinking_config.pop("thinkingLevel", None) + thinking_config = dict(existing or {}) if profile.thinking == "route": # 思考深度由模型 ID 决定,level/budget 会与之冲突 + thinking_config.pop("thinkingLevel", None) thinking_config.pop("thinkingBudget", None) - else: # "budget" + elif profile.thinking == "budget": + thinking_config.pop("thinkingLevel", None) thinking_config["thinkingBudget"] = profile.thinking_budget thinking_config["includeThoughts"] = return_thoughts generation_config["thinkingConfig"] = thinking_config @@ -866,8 +870,9 @@ async def normalize_antigravity_request( model = result.get("model", "") generation_config = (result.get("generationConfig") or {}).copy() - # 项目固定为 True(原版从 config 读取 return_thoughts,本项目无此配置项) - return_thoughts = True + # 是否向客户端回传思维链:由面板开关或环境变量 RETURN_THOUGHTS_TO_FRONTEND 控制(默认 True) + from config import get_return_thoughts_to_frontend + return_thoughts = await get_return_thoughts_to_frontend() # ========== 0.5 模型名复写(面板可配,热更新) ========== # 在抗截断前缀剥离之后、profile 解析之前应用: diff --git a/src/thought_filter.py b/src/thought_filter.py new file mode 100644 index 000000000..34ec3221e --- /dev/null +++ b/src/thought_filter.py @@ -0,0 +1,82 @@ +""" +思维链片段过滤。 + +后端不遵守 thinkingConfig.includeThoughts 时,在响应侧剔除 thought:true 的片段, +使“返回思维链到前端”开关在关闭时依然生效。 +""" +import json +from typing import Any, Dict, Union + + +def strip_thought_parts(payload: Dict[str, Any]) -> bool: + """移除 candidates[].content.parts[] 中 thought==true 的片段,返回是否发生改动。""" + candidates = payload.get("candidates") + if not isinstance(candidates, list): + return False + changed = False + for candidate in candidates: + if not isinstance(candidate, dict): + continue + content = candidate.get("content") + if not isinstance(content, dict): + continue + parts = content.get("parts") + if not isinstance(parts, list): + continue + kept = [part for part in parts if not (isinstance(part, dict) and part.get("thought") is True)] + if len(kept) != len(parts): + content["parts"] = kept + changed = True + return changed + + +def _target_payload(data: Any) -> Any: + """取出真正含 candidates 的载荷,兼容 {"response": {...}} 包装。""" + if isinstance(data, dict) and isinstance(data.get("response"), dict) and "candidates" not in data: + return data["response"] + return data + + +def filter_sse_chunk(chunk: Union[str, bytes]) -> Union[str, bytes]: + """过滤单条 Gemini SSE data 块的思考片段;无法解析或无需过滤时原样返回(保持 bytes/str 类型)。""" + if isinstance(chunk, bytes): + try: + text = chunk.decode("utf-8") + except Exception: + return chunk + elif isinstance(chunk, str): + text = chunk + else: + return chunk + + stripped = text.strip() + if not stripped.startswith("data:"): + return chunk + body = stripped[len("data:"):].strip() + if not body or body == "[DONE]": + return chunk + try: + data = json.loads(body) + except Exception: + return chunk + + target = _target_payload(data) + if not isinstance(target, dict) or not strip_thought_parts(target): + return chunk + + out = f"data: {json.dumps(data, ensure_ascii=False)}\n\n" + return out.encode("utf-8") if isinstance(chunk, bytes) else out + + +def filter_response_bytes(content: bytes) -> bytes: + """过滤非流式完整响应体中的思考片段;无法解析或无需过滤时原样返回。""" + if not content: + return content + try: + data = json.loads(content.decode("utf-8")) + except Exception: + return content + target = _target_payload(data) + if not isinstance(target, dict) or not strip_thought_parts(target): + return content + return json.dumps(data, ensure_ascii=False).encode("utf-8")