Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/api/antigravity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

# 流式请求完成,检查结果
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
)
Expand Down
19 changes: 12 additions & 7 deletions src/converter/antigravity_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 解析之前应用:
Expand Down
82 changes: 82 additions & 0 deletions src/thought_filter.py
Original file line number Diff line number Diff line change
@@ -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")
Loading