返回提交历史
Modified
g4f/mcp/server.py
+1
-4
Modified
g4f/tools/optimize_request.py
+23
-218
Modified
g4f/tools/run_tools.py
+2
-2
XFEstudio/gpt4free
Improve tool usage
5a438a63
代码差异
3 个文件
+26
-224
@@ -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",
@@ -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)
@@ -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