fix: issues with empty tool calls and handling excessively long tool results

This commit is contained in:
zhayujie
2026-02-07 20:25:05 +08:00
parent 5264f7ce18
commit 0542700f90
4 changed files with 71 additions and 19 deletions

View File

@@ -350,6 +350,15 @@ class AgentStreamExecutor:
# Fallback to full JSON
result_content = json.dumps(result, ensure_ascii=False)
# Truncate large tool results to prevent message bloat
# Keep tool results under 50KB to avoid context window issues
MAX_TOOL_RESULT_CHARS = 20000
if len(result_content) > MAX_TOOL_RESULT_CHARS:
truncated_len = len(result_content)
result_content = result_content[:MAX_TOOL_RESULT_CHARS] + \
f"\n\n[Output truncated: {truncated_len} chars total, showing first {MAX_TOOL_RESULT_CHARS} chars]"
logger.info(f"📎 Truncated tool result for '{tool_call['name']}': {truncated_len} -> {MAX_TOOL_RESULT_CHARS} chars")
tool_result_block = {
"type": "tool_result",
"tool_use_id": tool_call["id"],
@@ -678,6 +687,14 @@ class AgentStreamExecutor:
tool_calls = []
for idx in sorted(tool_calls_buffer.keys()):
tc = tool_calls_buffer[idx]
# Ensure tool call has a valid ID (some providers return empty/None IDs)
tool_id = tc.get("id") or ""
if not tool_id:
import uuid
tool_id = f"call_{uuid.uuid4().hex[:24]}"
logger.debug(f"⚠️ Tool call missing ID for '{tc.get('name')}', generated fallback: {tool_id}")
try:
# Safely get arguments, handle None case
args_str = tc.get("arguments") or ""
@@ -694,7 +711,7 @@ class AgentStreamExecutor:
# Return a clear error message to the LLM instead of empty dict
# This helps the LLM understand what went wrong
tool_calls.append({
"id": tc["id"],
"id": tool_id,
"name": tc["name"],
"arguments": {},
"_parse_error": f"Invalid JSON in tool arguments: {args_preview}... Error: {str(e)}. Tip: For large content, consider splitting into smaller chunks or using a different approach."
@@ -702,7 +719,7 @@ class AgentStreamExecutor:
continue
tool_calls.append({
"id": tc["id"],
"id": tool_id,
"name": tc["name"],
"arguments": arguments
})

View File

@@ -628,10 +628,29 @@ def _handle_linkai_stream_response(self, base_url, headers, body):
break
try:
chunk = json.loads(line)
yield chunk
except json.JSONDecodeError:
continue
# Check for error responses within the stream
# Some providers (e.g., MiniMax via LinkAI) return errors as:
# {'type': 'error', 'error': {'type': '...', 'message': '...', 'http_code': '400'}}
if chunk.get("type") == "error" or (
isinstance(chunk.get("error"), dict) and "message" in chunk.get("error", {})
):
error_data = chunk.get("error", {})
error_msg = error_data.get("message", "Unknown error") if isinstance(error_data, dict) else str(error_data)
http_code = error_data.get("http_code", "") if isinstance(error_data, dict) else ""
status_code = int(http_code) if http_code and str(http_code).isdigit() else 400
logger.error(f"[LinkAI] stream error: {error_msg} (http_code={http_code})")
yield {
"error": True,
"message": error_msg,
"status_code": status_code
}
return
yield chunk
except Exception as e:
logger.error(f"[LinkAI] stream response error: {e}")
yield {

View File

@@ -285,10 +285,16 @@ class MinimaxBot(Bot):
text_parts.append(block.get("text", ""))
elif block.get("type") == "tool_result":
# Tool result should be a separate message with role="tool"
tool_call_id = block.get("tool_use_id") or ""
if not tool_call_id:
logger.warning(f"[MINIMAX] tool_result missing tool_use_id")
result_content = block.get("content", "")
if not isinstance(result_content, str):
result_content = json.dumps(result_content, ensure_ascii=False)
tool_results.append({
"role": "tool",
"tool_call_id": block.get("tool_use_id"),
"content": str(block.get("content", ""))
"tool_call_id": tool_call_id,
"content": result_content
})
if text_parts:

View File

@@ -242,10 +242,17 @@ class OpenAICompatibleBot:
# First, add tool result messages (must come immediately after assistant with tool_calls)
for block in tool_results:
tool_call_id = block.get("tool_use_id") or ""
if not tool_call_id:
logger.warning(f"[OpenAICompatible] tool_result missing tool_use_id, using empty string")
# Ensure content is a string (some providers require string content)
result_content = block.get("content", "")
if not isinstance(result_content, str):
result_content = json.dumps(result_content, ensure_ascii=False)
openai_messages.append({
"role": "tool",
"tool_call_id": block.get("tool_use_id"),
"content": block.get("content", "")
"tool_call_id": tool_call_id,
"content": result_content
})
# Then, add text content as a separate user message if present
@@ -265,8 +272,11 @@ class OpenAICompatibleBot:
if block.get("type") == "text":
text_parts.append(block.get("text", ""))
elif block.get("type") == "tool_use":
tool_id = block.get("id") or ""
if not tool_id:
logger.warning(f"[OpenAICompatible] tool_use missing id for '{block.get('name')}'")
tool_calls.append({
"id": block.get("id"),
"id": tool_id,
"type": "function",
"function": {
"name": block.get("name"),