feat: support new model Grok 4.20 Beta

# Conflicts:
#	app/services/grok/services/chat.py
This commit is contained in:
Chenyme authored and XianYuDaXian committed 2026-02-18 23:29:48 +08:00
1 parent bac0ed508c
commit b6b753b97b
8 files changed
+263 -5

No files matched your search

+5 -1
View File
@@ -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]}..."
)
+138 -2
View File
@@ -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)
+11
View File
@@ -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",
+10 -2
View File
@@ -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
+31
View File
@@ -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();
}
})();
+3
View File
@@ -5,6 +5,9 @@
> [!NOTE]
> This project is for learning and research only. You must comply with Grok **Terms of Use** and **local laws and regulations**. Do not use for illegal purposes.
> [!NOTE]
> Open source projects welcome everyone's support for secondary development and pull requests, but please retain the original author's and frontend's logos to respect the work of others!
Grok2API rebuilt with **FastAPI**, fully aligned with the latest web call format. Supports streaming/non-streaming chat, image generation/editing, video generation/upscale (text-to-video and image-to-video), deep reasoning, token pool concurrency, and automatic load balancing.
<img width="2618" height="1658" alt="image" src="https://github.com/user-attachments/assets/a8c406f8-4c28-483a-8099-c23df5df7605" />
+3
View File
@@ -5,6 +5,9 @@
> [!NOTE]
> 本项目仅供学习与研究,使用者必须在遵循 Grok 的 **使用条款** 以及 **法律法规** 的情况下使用,不得用于非法用途。
> [!NOTE]
> 开源项目欢迎大家支持二开和PR,但请保留原作者标识和前端标识,尊重他人劳动成果~!
基于 **FastAPI** 重构的 Grok2API,全面适配最新 Web 调用格式,支持流/非流式对话、图像生成/编辑、视频生成/超分(文生视频 / 图生视频)、深度思考,号池并发与自动负载均衡一体化。
<img width="2618" height="1658" alt="image" src="https://github.com/user-attachments/assets/a8c406f8-4c28-483a-8099-c23df5df7605" />
+62
View File
@@ -0,0 +1,62 @@
"""
Simple usage probe to inspect /rest/rate-limits response.
Usage:
python scripts/test_usage_response.py
(optional) TOKEN_POOL=ssoBasic|ssoSuper
"""
import asyncio
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from curl_cffi.requests import AsyncSession
from app.core.config import config
from app.services.reverse.rate_limits import RateLimitsReverse
from app.services.token import get_token_manager
async def main() -> int:
await config.load()
token = None
pool = os.getenv("TOKEN_POOL")
manager = await get_token_manager()
await manager.reload_if_stale()
if pool:
token = manager.get_token(pool_name=pool)
else:
token = manager.get_token(pool_name="ssoBasic") or manager.get_token(
pool_name="ssoSuper"
)
if not token:
token = os.getenv("GROK_TOKEN") or os.getenv("SSO_TOKEN") or os.getenv("TOKEN")
if not token:
print("Missing token. Ensure token pool is configured or set GROK_TOKEN.")
return 2
async with AsyncSession() as session:
response = await RateLimitsReverse.request(session, token)
try:
data = response.json()
except Exception as exc:
print(f"Failed to parse JSON: {exc}")
raw = getattr(response, "text", "")
if raw:
print(raw)
return 3
print(data)
return 0
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))