XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

Improve tool usage

5a438a63
hlohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

3 个文件 +26 -224
Modified g4f/mcp/server.py +1 -4
@@ -30,9 +30,8 @@ from .tools import (
30 30 MarkItDownTool, TextToAudioTool, WebSearchTool, ImageGenerationTool,
31 31 PythonExecuteTool, FileReadTool,
32 32 FileListTool, FileDeleteTool, ApplyPatchTool,
33 CreateDirectoryTool, CreateFileTool, FileWriteTool, FetchWebpageTool,
33 CreateFileTool, FileWriteTool, FetchWebpageTool,
34 34 FileSearchGlobTool, GrepSearchTool, GithubRepoTool, GithubTextSearchTool,
35 TokenOptimizerTool,
36 35 )
37 36
38 37
@@ -81,7 +80,6 @@ class MCPServer:
81 80 'file_read': FileReadTool(),
82 81 'file_list': FileListTool(safe_mode=safe_mode),
83 82 'file_delete': FileDeleteTool(),
84 'create_directory': CreateDirectoryTool(),
85 83 'create_file': CreateFileTool(),
86 84 'file_write': FileWriteTool(),
87 85 'fetch_webpage': FetchWebpageTool(),
@@ -89,7 +87,6 @@ class MCPServer:
89 87 'grep_search': GrepSearchTool(),
90 88 'github_repo': GithubRepoTool(),
91 89 'github_text_search': GithubTextSearchTool(),
92 'token_optimizer': TokenOptimizerTool(),
93 90 }
94 91 self.server_info = {
95 92 "name": "gpt4free-mcp-server",
Modified g4f/tools/optimize_request.py +23 -218
@@ -14,6 +14,23 @@ from typing import Any, Dict, List, Tuple
14 14
15 15 from ..typing import Messages
16 16
17 _MAX_TOOL_REPEATS = 3 # max times the same tool call may appear before breaking
18
19 # Cap the byte size of any single tool result / function call output embedded
20 # in the conversation. Older results are rarely re-read by the model but still
21 # consume the full input budget on every turn.
22 _TOOL_RESULT_CAP = 4096 # bytes per tool result
23 _OLD_TOOL_RESULT_CAP = 1200 # stricter cap for results older than 2 turns
24
25 _WS_RE = re.compile(r"[ \t]+\n")
26 _BLANK_RUN_RE = re.compile(r"\n{3,}")
27 _MAX_TURNS = 40 # keep at most this many non-system messages
28
29 # Cap on how many bytes of a *single* tool-result message we keep.
30 # Anything longer is truncated to head+tail with an omission marker.
31 _TOOL_RESULT_CAP = 4000 # ≈1000 tokens per tool result is plenty for context
32 _TOOL_RESULT_HEAD = 1500
33 _TOOL_RESULT_TAIL = 1500
17 34
18 35 # ── System prompt condensation ──────────────────────────────────────────────
19 36 # Replaces the verbose Copilot system preamble with a condensed version.
@@ -732,8 +749,6 @@ def dedup_messages(messages: Messages) -> tuple[Messages, int]:
732 749
733 750 # ── Tool-loop detection ─────────────────────────────────────────────────────
734 751
735 _MAX_TOOL_REPEATS = 3 # max times the same tool call may appear before breaking
736
737 752
738 753 def _tool_call_signature(tool_calls: list) -> str:
739 754 """Build a stable signature from a list of tool calls.
@@ -923,12 +938,6 @@ def strip_reasoning_echo(messages: Messages) -> int:
923 938
924 939 # ── Tool result truncation ──────────────────────────────────────────────────
925 940
926 # Cap the byte size of any single tool result / function call output embedded
927 # in the conversation. Older results are rarely re-read by the model but still
928 # consume the full input budget on every turn.
929 _TOOL_RESULT_CAP = 4096 # bytes per tool result
930 _OLD_TOOL_RESULT_CAP = 1200 # stricter cap for results older than 2 turns
931
932 941
933 942 def _truncate_tool_results(messages: Messages) -> int:
934 943 """Truncate oversized tool/function call results in place.
@@ -1047,9 +1056,6 @@ def _strip_redundant_tool_fields(messages: Messages) -> int:
1047 1056
1048 1057 # ── Collapse whitespace in message content ──────────────────────────────────
1049 1058
1050 _WS_RE = re.compile(r"[ \t]+\n")
1051 _BLANK_RUN_RE = re.compile(r"\n{3,}")
1052
1053 1059
1054 1060 def _collapse_message_whitespace(messages: Messages) -> int:
1055 1061 """Normalize trailing whitespace and repeated blank lines in all messages.
@@ -1087,9 +1093,6 @@ def _collapse_message_whitespace(messages: Messages) -> int:
1087 1093
1088 1094 # ── Drop stale context (old user turns beyond a threshold) ───────────────────
1089 1095
1090 _MAX_TURNS = 40 # keep at most this many non-system messages
1091
1092
1093 1096 def _trim_old_turns(messages: Messages) -> int:
1094 1097 """Drop the oldest non-system messages when the conversation is very long.
1095 1098
@@ -1118,153 +1121,10 @@ def _trim_old_turns(messages: Messages) -> int:
1118 1121 messages[:] = system_msgs + kept
1119 1122 return max(0, saved_bytes)
1120 1123
1121
1122 def optimize_request(messages: Messages, tools: Any) -> Tuple[int, Dict[str, str]]:
1123 """Truncate very old tool-result messages to a head+tail snippet.
1124
1125 Only messages *before* the last user/assistant turn are truncated so the
1126 most recent context is preserved verbatim. Returns bytes saved.
1127 """
1128 if not messages:
1129 return 0
1130
1131 # Find the index of the last "fresh" turn — the last user or assistant
1132 # message that is not an empty tool-response. Messages before that
1133 # index are candidates for truncation.
1134 last_fresh = -1
1135 for i in range(len(messages) - 1, -1, -1):
1136 msg = messages[i]
1137 if not isinstance(msg, dict):
1138 continue
1139 role = msg.get("role")
1140 if role in ("user", "assistant") and msg.get("content"):
1141 last_fresh = i
1142 break
1143 if last_fresh <= 0:
1144 return 0
1145
1146 saved_bytes = 0
1147 for i in range(0, last_fresh):
1148 msg = messages[i]
1149 if not isinstance(msg, dict) or msg.get("role") != "tool":
1150 continue
1151 content = msg.get("content")
1152 if not isinstance(content, str):
1153 continue
1154 raw = content.encode("utf-8", errors="replace")
1155 if len(raw) <= _TOOL_RESULT_CAP:
1156 continue
1157 head = raw[:_TOOL_RESULT_KEEP_HEAD].decode("utf-8", errors="replace")
1158 tail = raw[-_TOOL_RESULT_KEEP_TAIL:].decode("utf-8", errors="replace")
1159 omitted = len(raw) - _TOOL_RESULT_KEEP_HEAD - _TOOL_RESULT_KEEP_TAIL
1160 msg["content"] = (
1161 f"{head}\n\n... [{omitted} bytes omitted — old tool result truncated] ...\n\n{tail}"
1162 )
1163 saved_bytes += len(raw) - len(msg["content"].encode("utf-8", errors="replace"))
1164 return max(0, saved_bytes)
1165
1166
1167 def _strip_redundant_tool_calls(messages: Messages) -> int:
1168 """Remove ``tool_calls`` from assistant messages once the corresponding
1169 tool result has been returned. Providers keep the full tool-call spec
1170 (function name + arguments) in history even after the result is in
1171 context, which wastes tokens. Returns bytes saved.
1172 """
1173 if not messages:
1174 return 0
1175 import json as _json
1176
1177 saved_bytes = 0
1178 for i, msg in enumerate(messages):
1179 if not isinstance(msg, dict) or msg.get("role") != "assistant":
1180 continue
1181 tool_calls = msg.get("tool_calls")
1182 if not tool_calls:
1183 continue
1184 # Is there a later ``tool`` message in the conversation? If so the
1185 # result is already in context and the call spec is redundant.
1186 has_result = any(
1187 isinstance(m, dict) and m.get("role") == "tool"
1188 for m in messages[i + 1:]
1189 )
1190 if not has_result:
1191 continue
1192 before = len(_json.dumps(msg, ensure_ascii=False).encode("utf-8", errors="replace"))
1193 # Keep only the id so providers can still correlate, strip verbose keys.
1194 stripped_calls = []
1195 for tc in tool_calls:
1196 if not isinstance(tc, dict):
1197 stripped_calls.append(tc)
1198 continue
1199 tc = {k: v for k, v in tc.items() if k not in _TOOL_CALL_STRIP_KEYS}
1200 # Replace the full arguments with a short marker — the actual
1201 # arguments are recoverable from the tool-result message.
1202 fn = tc.get("function")
1203 if isinstance(fn, dict) and fn.get("arguments"):
1204 fn = {k: v for k, v in fn.items() if k != "arguments"}
1205 fn["arguments"] = "{}"
1206 tc["function"] = fn
1207 stripped_calls.append(tc)
1208 msg["tool_calls"] = stripped_calls
1209 after = len(_json.dumps(msg, ensure_ascii=False).encode("utf-8", errors="replace"))
1210 saved_bytes += max(0, before - after)
1211 return saved_bytes
1212
1213
1214 def _collapse_whitespace(messages: Messages) -> int:
1215 """Collapse runs of blank lines and trailing whitespace inside every
1216 string content. Returns bytes saved.
1217 """
1218 saved_bytes = 0
1219 for msg in messages:
1220 if not isinstance(msg, dict):
1221 continue
1222 content = msg.get("content")
1223 if not isinstance(content, str):
1224 continue
1225 new = re.sub(r"[ \t]+\n", "\n", content) # trailing spaces
1226 new = re.sub(r"\n{3,}", "\n\n", new) # blank-line runs
1227 new = new.strip()
1228 if len(new) < len(content):
1229 saved_bytes += len(content.encode("utf-8", errors="replace")) - len(new.encode("utf-8", errors="replace"))
1230 if new:
1231 msg["content"] = new
1232 else:
1233 msg["content"] = ""
1234 return max(0, saved_bytes)
1235
1236
1237 1124 # ──────────────────────────────────────────────────────────────────────
1238 1125 # New optimizers
1239 1126 # ──────────────────────────────────────────────────────────────────────
1240 1127
1241 # Cap on how many bytes of a *single* tool-result message we keep.
1242 # Anything longer is truncated to head+tail with an omission marker.
1243 _TOOL_RESULT_CAP = 4000 # ≈1000 tokens per tool result is plenty for context
1244
1245 _TOOL_RESULT_HEAD = 1500
1246 _TOOL_RESULT_TAIL = 1500
1247
1248 # Fields on message dicts that some providers echo back but that are not
1249 # needed for the next turn (the assistant already produced them).
1250 _DROP_ASSISTANT_FIELDS = ("tool_calls", "function_call", "name", "refusal")
1251
1252
1253 def _content_bytes(content: Any) -> int:
1254 """Byte length of a message's content (str or list of parts)."""
1255 if isinstance(content, str):
1256 return len(content.encode("utf-8", errors="replace"))
1257 if isinstance(content, list):
1258 total = 0
1259 for part in content:
1260 if isinstance(part, dict):
1261 total += len(str(part.get("text", "")).encode("utf-8", errors="replace"))
1262 elif isinstance(part, str):
1263 total += len(part.encode("utf-8", errors="replace"))
1264 return total
1265 return 0
1266
1267
1268 1128 def _set_content(msg: dict, content: Any) -> None:
1269 1129 """Set content on a message dict, handling both str and list forms."""
1270 1130 msg["content"] = content
@@ -1299,61 +1159,7 @@ def truncate_tool_results(messages: Messages) -> int:
1299 1159 _set_content(msg, new_content)
1300 1160 return max(0, saved)
1301 1161
1302
1303 def strip_redundant_assistant_fields(messages: Messages) -> int:
1304 """Remove fields on assistant messages that are no longer needed.
1305
1306 Once an assistant turn is in the history, the ``tool_calls`` /
1307 ``function_call`` metadata is redundant — the corresponding ``tool``
1308 messages that follow already carry the result. Dropping these fields
1309 avoids providers re-serialising them on every turn.
1310
1311 Returns bytes saved (approximated from JSON length).
1312 """
1313 import json as _json
1314
1315 saved = 0
1316 for msg in messages:
1317 if not isinstance(msg, dict) or msg.get("role") != "assistant":
1318 continue
1319 before = len(_json.dumps(msg, ensure_ascii=False).encode("utf-8", errors="replace"))
1320 changed = False
1321 for field in _DROP_ASSISTANT_FIELDS:
1322 if field in msg:
1323 # Only drop tool_calls if there is content to keep, so we
1324 # never produce an empty assistant message.
1325 if field == "tool_calls" and not msg.get("content"):
1326 continue
1327 del msg[field]
1328 changed = True
1329 if changed:
1330 after = len(_json.dumps(msg, ensure_ascii=False).encode("utf-8", errors="replace"))
1331 saved += before - after
1332 return max(0, saved)
1333
1334
1335 def collapse_whitespace_in_messages(messages: Messages) -> int:
1336 """Collapse runs of 3+ newlines and trailing whitespace inside message content.
1337
1338 Returns bytes saved.
1339 """
1340 saved = 0
1341 for msg in messages:
1342 if not isinstance(msg, dict):
1343 continue
1344 content = msg.get("content")
1345 if not isinstance(content, str):
1346 continue
1347 new = re.sub(r"[ \t]+\n", "\n", content) # trailing spaces on lines
1348 new = re.sub(r"\n{3,}", "\n\n", new) # 3+ newlines → 2
1349 new = new.strip()
1350 if len(new) != len(content):
1351 saved += len(content.encode("utf-8", errors="replace")) - len(new.encode("utf-8", errors="replace"))
1352 _set_content(msg, new)
1353 return max(0, saved)
1354
1355
1356 def drop_empty_trailing_messages(messages: Messages) -> int:
1162 def _drop_empty_trailing_messages(messages: Messages) -> int:
1357 1163 """Drop trailing messages with empty content (no value for the next turn).
1358 1164
1359 1165 Returns bytes saved (always 0 — these are empty, but we count messages
@@ -1402,12 +1208,6 @@ def optimize_request(messages: Messages, tools: Any) -> Tuple[int, Dict[str, str
1402 1208 saved_bytes += dedup_saved
1403 1209 logs["dedup"] = f"removed duplicate/empty messages (-{dedup_saved} bytes)"
1404 1210
1405 # ── Break tool-call loops ──
1406 # loop_saved = break_tool_loop(messages)
1407 # if loop_saved:
1408 # saved_bytes += loop_saved
1409 # logs["tool_loop"] = f"broke tool-call loop (-{loop_saved} bytes)"
1410
1411 1211 echo_saved = strip_reasoning_echo(messages)
1412 1212 if echo_saved:
1413 1213 saved_bytes += echo_saved
@@ -1437,6 +1237,11 @@ def optimize_request(messages: Messages, tools: Any) -> Tuple[int, Dict[str, str
1437 1237 saved_bytes += trim_saved
1438 1238 logs["trim_old"] = f"dropped {trim_saved} bytes of stale turns"
1439 1239
1240 # ── Drop empty trailing messages ──
1241 empty_removed = _drop_empty_trailing_messages(messages)
1242 if empty_removed:
1243 logs["empty_trailing"] = f"dropped {empty_removed} empty trailing message(s)"
1244
1440 1245 # ── Tools ──
1441 1246 if isinstance(tools, list) and tools:
1442 1247 filtered, tool_saved, tool_logs = optimize_tools(tools)
Modified g4f/tools/run_tools.py +2 -2
@@ -47,7 +47,7 @@ from .. import debug
47 47
48 48 _conversation_cache: dict[str, dict] = {}
49 49 _CACHE_MAX_SIZE = 128
50 _CACHE_TTL = 3600.0 # seconds
50 _CACHE_TTL = 3600 * 12 # 1h * 12 = 12h
51 51
52 52
53 53 def _messages_cache_key(messages: Messages, model: str) -> Optional[str]:
@@ -422,7 +422,7 @@ async def async_iter_run_tools(
422 422 response = wait_for(response, timeout=timeout) if stream else response
423 423
424 424 try:
425 usage_model = model
425 usage_model = model or getattr(provider, "default_model", model)
426 426 usage_provider = provider.__name__
427 427 completion_tokens = 0
428 428 usage = None