feat: support new model Grok 4.20 Beta
# Conflicts: # app/services/grok/services/chat.py
This commit is contained in:
1 parent
bac0ed508c
commit
b6b753b97b
8 files changed
+263
-5
No files matched your search
@@ -46,7 +46,11 @@ class UsageService:
|
||||
async with AsyncSession() as session:
|
||||
response = await RateLimitsReverse.request(session, token)
|
||||
data = response.json()
|
||||
remaining = data.get("remainingTokens", 0)
|
||||
remaining = data.get("remainingTokens")
|
||||
if remaining is None:
|
||||
remaining = data.get("remainingQueries")
|
||||
if remaining is not None:
|
||||
data["remainingTokens"] = remaining
|
||||
logger.info(
|
||||
f"Usage sync success: remaining={remaining}, token={token[:10]}..."
|
||||
)
|
||||
|
||||
@@ -33,6 +33,61 @@ _CHAT_SEMAPHORE = None
|
||||
_CHAT_SEM_VALUE = None
|
||||
|
||||
|
||||
def extract_tool_text(raw: str) -> str:
|
||||
if not raw:
|
||||
return ""
|
||||
name_match = re.search(
|
||||
r"<xai:tool_name>(.*?)</xai:tool_name>", raw, flags=re.DOTALL
|
||||
)
|
||||
args_match = re.search(
|
||||
r"<xai:tool_args>(.*?)</xai:tool_args>", raw, flags=re.DOTALL
|
||||
)
|
||||
|
||||
name = name_match.group(1) if name_match else ""
|
||||
if name:
|
||||
name = re.sub(r"<!\[CDATA\[(.*?)\]\]>", r"\1", name, flags=re.DOTALL).strip()
|
||||
|
||||
args = args_match.group(1) if args_match else ""
|
||||
if args:
|
||||
args = re.sub(r"<!\[CDATA\[(.*?)\]\]>", r"\1", args, flags=re.DOTALL).strip()
|
||||
|
||||
payload = None
|
||||
if args:
|
||||
try:
|
||||
payload = orjson.loads(args)
|
||||
except orjson.JSONDecodeError:
|
||||
payload = None
|
||||
|
||||
label = name
|
||||
text = args
|
||||
if name == "web_search":
|
||||
label = "[WebSearch]"
|
||||
if isinstance(payload, dict):
|
||||
text = payload.get("query") or payload.get("q") or ""
|
||||
elif name == "search_images":
|
||||
label = "[SearchImage]"
|
||||
if isinstance(payload, dict):
|
||||
text = (
|
||||
payload.get("image_description")
|
||||
or payload.get("description")
|
||||
or payload.get("query")
|
||||
or ""
|
||||
)
|
||||
elif name == "chatroom_send":
|
||||
label = "[AgentThink]"
|
||||
if isinstance(payload, dict):
|
||||
text = payload.get("message") or ""
|
||||
|
||||
if label and text:
|
||||
return f"{label} {text}".strip()
|
||||
if label:
|
||||
return label
|
||||
if text:
|
||||
return text
|
||||
# Fallback: strip tags to keep any raw text.
|
||||
return re.sub(r"<[^>]+>", "", raw, flags=re.DOTALL).strip()
|
||||
|
||||
|
||||
def _get_chat_semaphore() -> asyncio.Semaphore:
|
||||
global _CHAT_SEMAPHORE, _CHAT_SEM_VALUE
|
||||
value = max(1, int(get_config("chat.concurrent")))
|
||||
@@ -116,7 +171,7 @@ class GrokChatService:
|
||||
token: str,
|
||||
message: str,
|
||||
model: str = "grok-3",
|
||||
requested_model: str | None = None,
|
||||
requested_model: str | None = None, (feat: support new model Grok 4.20 Beta)
|
||||
mode: str = None,
|
||||
stream: bool = None,
|
||||
file_attachments: List[str] = None,
|
||||
@@ -354,15 +409,82 @@ class StreamProcessor(proc_base.BaseProcessor):
|
||||
self.think_opened: bool = False
|
||||
self.role_sent: bool = False
|
||||
self.filter_tags = get_config("app.filter_tags")
|
||||
self.tool_usage_enabled = (
|
||||
"xai:tool_usage_card" in (self.filter_tags or [])
|
||||
)
|
||||
self._tool_usage_opened = False
|
||||
self._tool_usage_buffer = ""
|
||||
|
||||
self.show_think = bool(show_think)
|
||||
|
||||
def _filter_tool_card(self, token: str) -> str:
|
||||
if not token or not self.tool_usage_enabled:
|
||||
return token
|
||||
|
||||
output_parts: list[str] = []
|
||||
rest = token
|
||||
start_tag = "<xai:tool_usage_card"
|
||||
end_tag = "</xai:tool_usage_card>"
|
||||
|
||||
while rest:
|
||||
if self._tool_usage_opened:
|
||||
end_idx = rest.find(end_tag)
|
||||
if end_idx == -1:
|
||||
self._tool_usage_buffer += rest
|
||||
return "".join(output_parts)
|
||||
end_pos = end_idx + len(end_tag)
|
||||
self._tool_usage_buffer += rest[:end_pos]
|
||||
line = extract_tool_text(self._tool_usage_buffer)
|
||||
if line:
|
||||
if output_parts and not output_parts[-1].endswith("\n"):
|
||||
output_parts[-1] += "\n"
|
||||
output_parts.append(f"{line}\n")
|
||||
self._tool_usage_buffer = ""
|
||||
self._tool_usage_opened = False
|
||||
rest = rest[end_pos:]
|
||||
continue
|
||||
|
||||
start_idx = rest.find(start_tag)
|
||||
if start_idx == -1:
|
||||
output_parts.append(rest)
|
||||
break
|
||||
|
||||
if start_idx > 0:
|
||||
output_parts.append(rest[:start_idx])
|
||||
|
||||
end_idx = rest.find(end_tag, start_idx)
|
||||
if end_idx == -1:
|
||||
self._tool_usage_opened = True
|
||||
self._tool_usage_buffer = rest[start_idx:]
|
||||
break
|
||||
|
||||
end_pos = end_idx + len(end_tag)
|
||||
raw_card = rest[start_idx:end_pos]
|
||||
line = extract_tool_text(raw_card)
|
||||
if line:
|
||||
if output_parts and not output_parts[-1].endswith("\n"):
|
||||
output_parts[-1] += "\n"
|
||||
output_parts.append(f"{line}\n")
|
||||
rest = rest[end_pos:]
|
||||
|
||||
return "".join(output_parts)
|
||||
|
||||
def _filter_token(self, token: str) -> str:
|
||||
"""Filter special tags in current token only."""
|
||||
if not self.filter_tags or not token:
|
||||
if not token:
|
||||
return token
|
||||
|
||||
if self.tool_usage_enabled:
|
||||
token = self._filter_tool_card(token)
|
||||
if not token:
|
||||
return ""
|
||||
|
||||
if not self.filter_tags:
|
||||
return token
|
||||
|
||||
for tag in self.filter_tags:
|
||||
if tag == "xai:tool_usage_card":
|
||||
continue
|
||||
if f"<{tag}" in token or f"</{tag}" in token:
|
||||
return ""
|
||||
|
||||
@@ -551,7 +673,21 @@ class CollectProcessor(proc_base.BaseProcessor):
|
||||
return content
|
||||
|
||||
result = content
|
||||
if "xai:tool_usage_card" in self.filter_tags:
|
||||
result = re.sub(
|
||||
r"<xai:tool_usage_card[^>]*>.*?</xai:tool_usage_card>",
|
||||
lambda match: (
|
||||
f"{extract_tool_text(match.group(0))}\n"
|
||||
if extract_tool_text(match.group(0))
|
||||
else ""
|
||||
),
|
||||
result,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
for tag in self.filter_tags:
|
||||
if tag == "xai:tool_usage_card":
|
||||
continue
|
||||
pattern = rf"<{re.escape(tag)}[^>]*>.*?</{re.escape(tag)}>|<{re.escape(tag)}[^>]*/>"
|
||||
result = re.sub(pattern, "", result, flags=re.DOTALL)
|
||||
|
||||
|
||||
@@ -163,6 +163,17 @@ class ModelService:
|
||||
is_image_edit=False,
|
||||
is_video=False,
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-4.20-beta",
|
||||
grok_model="grok-420",
|
||||
model_mode="MODEL_MODE_GROK_420",
|
||||
tier=Tier.BASIC,
|
||||
cost=Cost.LOW,
|
||||
display_name="GROK-4.20-BETA",
|
||||
is_image=False,
|
||||
is_image_edit=False,
|
||||
is_video=False,
|
||||
),
|
||||
ModelInfo(
|
||||
model_id="grok-imagine-1.0",
|
||||
grok_model="grok-3",
|
||||
|
||||
@@ -387,8 +387,12 @@ class TokenManager:
|
||||
result = await usage_service.get(token_str)
|
||||
|
||||
if result and "remainingTokens" in result:
|
||||
new_quota = result.get("remainingTokens")
|
||||
if new_quota is None:
|
||||
new_quota = result.get("remainingQueries")
|
||||
if new_quota is None:
|
||||
return False
|
||||
old_quota = target_token.quota
|
||||
new_quota = result["remainingTokens"]
|
||||
|
||||
target_token.update_quota(new_quota)
|
||||
target_token.record_success(is_usage=is_usage)
|
||||
@@ -711,7 +715,11 @@ class TokenManager:
|
||||
result = await usage_service.get(token_str)
|
||||
|
||||
if result and "remainingTokens" in result:
|
||||
new_quota = result["remainingTokens"]
|
||||
new_quota = result.get("remainingTokens")
|
||||
if new_quota is None:
|
||||
new_quota = result.get("remainingQueries")
|
||||
if new_quota is None:
|
||||
return {"recovered": False, "expired": False}
|
||||
old_quota = token_info.quota
|
||||
old_status = token_info.status
|
||||
|
||||
|
||||
@@ -45,3 +45,34 @@ function showToast(message, type = 'success') {
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
(function showRateLimitNoticeOnce() {
|
||||
const noticeKey = 'grok2api_rate_limits_notice_v1';
|
||||
const noticeText = 'GROK官方服务 rate-limits 更新后暂时无法准确计算 Token 剩余,等待官方接口优化后持续修复';
|
||||
const path = window.location.pathname || '';
|
||||
|
||||
if (!path.startsWith('/admin') || path.startsWith('/admin/login')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (localStorage.getItem(noticeKey)) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(noticeKey, '1');
|
||||
} catch (e) {
|
||||
// If storage is blocked, just skip the one-time guard.
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast(noticeText, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', show);
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user