Skip to content
Open
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
36 changes: 29 additions & 7 deletions app/services/grok/services/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,8 +537,8 @@ def __init__(
self.fingerprint: str = ""
self.rollout_id: str = ""
self.think_opened: bool = False
self.think_closed_once: bool = False
self.image_think_active: bool = False
self._content_started: bool = False
self.role_sent: bool = False
self.filter_tags = get_config("app.filter_tags")
self.tool_usage_enabled = (
Expand Down Expand Up @@ -816,7 +816,6 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N
if self.image_think_active and self.think_opened:
yield self._sse("\n</think>\n")
self.think_opened = False
self.think_closed_once = True
self.image_think_active = False
for url in proc_base._collect_images(mr):
parts = url.split("/")
Expand Down Expand Up @@ -860,15 +859,26 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N
if (token := resp.get("token")) is not None:
if not token:
continue
if is_thinking and self.think_closed_once and not self.image_think_active:
continue
filtered = self._filter_token(token)
if not filtered:
continue
# 判断是否在 Agent 思考/处理阶段:
# - isThinking=true → 归入 think
# - 有 messageStepId → Agent 处理中,归入 think
# - image_think_active → 图片生成中
# 正式内容开始后,丢弃中途插入的思考(Grok 官网也隐藏了这部分)
has_step_id = bool(resp.get("messageStepId"))
in_think = (
(is_thinking and not self.think_closed_once)
is_thinking
or has_step_id
or self.image_think_active
)
# 正式内容已开始后,丢弃中途插入的 Agent 思考(1-2 句内部注释,无用户价值)
if self._content_started and in_think and not self.image_think_active:
continue
# 空 token 不关闭 think 块(搜索结果间的空 token 不算正式内容)
if not in_think and not filtered.strip():
continue
if in_think:
if not self.show_think:
continue
Expand All @@ -879,7 +889,7 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N
if self.think_opened:
yield self._sse("\n</think>\n")
self.think_opened = False
self.think_closed_once = True
self._content_started = True

if in_think:
self._record_content(filtered)
Expand All @@ -901,7 +911,6 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N

if self.think_opened:
yield self._sse("</think>\n")
self.think_closed_once = True

if self._tool_stream_enabled:
for kind, payload in self._flush_tool_stream():
Expand Down Expand Up @@ -1022,6 +1031,8 @@ async def process(self, response: AsyncIterable[bytes]) -> dict[str, Any]:
response_id = ""
fingerprint = ""
content = ""
# 兜底收集非 thinking 且无 messageStepId 的最终内容 token
fallback_tokens: list[str] = []
idle_timeout = get_config("chat.stream_timeout")

try:
Expand All @@ -1041,6 +1052,13 @@ async def process(self, response: AsyncIterable[bytes]) -> dict[str, Any]:
if (llm := resp.get("llmInfo")) and not fingerprint:
fingerprint = llm.get("modelHash", "")

# 收集非 thinking 且无 messageStepId 的 token(最终内容兜底)
is_thinking = bool(resp.get("isThinking"))
has_step_id = bool(resp.get("messageStepId"))
if not is_thinking and not has_step_id:
if tok := resp.get("token"):
fallback_tokens.append(tok)

if mr := resp.get("modelResponse"):
response_id = mr.get("responseId", "")
content = mr.get("message", "")
Expand Down Expand Up @@ -1140,6 +1158,10 @@ def _render_card(match: re.Match) -> str:
finally:
await self.close()

# modelResponse.message 为空时(多智能体模型),用兜底 token 拼接
if not content and fallback_tokens:
content = "".join(fallback_tokens)

content = self._filter_content(content)

# Parse for tool calls if tools were provided
Expand Down
22 changes: 20 additions & 2 deletions app/services/reverse/app_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ def _resolve_custom_personality() -> Optional[str]:
return None
return value

# modelMode → modeId 映射(Grok Web 新 API 格式)
# 基于浏览器前端 JS 逆向和 API 全量测试验证:
# 付费 SuperGrok 的多智能体模式需要 modeId 字段才能正常响应
# 有 modeId 时不发 modelName/modelMode(浏览器前端逻辑)
_MODE_ID_MAP = {
"MODEL_MODE_FAST": "fast",
"MODEL_MODE_EXPERT": "expert",
"MODEL_MODE_HEAVY": "heavy",
"MODEL_MODE_GROK_420": "expert",
"MODEL_MODE_GROK_4_1_THINKING": "expert",
"MODEL_MODE_GROK_4_1_MINI_THINKING": "expert",
}

@staticmethod
def build_payload(
message: str,
Expand Down Expand Up @@ -152,8 +165,13 @@ def build_payload(
"toolOverrides": tool_overrides or {},
}

if model == "grok-420":
payload["enable420"] = True
# 优先使用 modeId(Grok 新 API 格式,付费号多智能体模式必需)
# 有 modeId 时移除 modelName/modelMode(浏览器前端逻辑)
mode_id = AppChatReverse._MODE_ID_MAP.get(mode)
if mode_id:
payload["modeId"] = mode_id
payload.pop("modelName", None)
payload.pop("modelMode", None)

custom_personality = AppChatReverse._resolve_custom_personality()
if custom_personality is not None:
Expand Down