diff --git a/scripts/configure-nginx-upload-limit.sh b/scripts/configure-nginx-upload-limit.sh new file mode 100755 index 0000000..2236bb3 --- /dev/null +++ b/scripts/configure-nginx-upload-limit.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 配置线上 nginx 上传体积限制。 +# 需要 sudo 权限;应用服务本身仍然用普通用户运行。 + +LIMIT="${CLAW_NGINX_CLIENT_MAX_BODY_SIZE:-300m}" +CONF_PATH="${CLAW_NGINX_UPLOAD_CONF:-/etc/nginx/conf.d/zk-data-agent-upload.conf}" + +if ! command -v nginx >/dev/null 2>&1; then + echo "WARN 未发现 nginx,跳过上传限制配置。" + exit 0 +fi + +if ! command -v sudo >/dev/null 2>&1; then + echo "ERROR 配置 nginx 需要 sudo。" >&2 + exit 1 +fi + +sudo tee "${CONF_PATH}" >/dev/null < dict[str, Any]: ) +def _tool_call_ids(message: dict[str, Any]) -> list[str]: + raw_tool_calls = message.get('tool_calls') + if not isinstance(raw_tool_calls, list): + return [] + ids: list[str] = [] + for index, raw_call in enumerate(raw_tool_calls): + if not isinstance(raw_call, dict): + continue + call_id = raw_call.get('id') + ids.append(call_id if isinstance(call_id, str) and call_id else f'call_{index}') + return ids + + +def _contiguous_tool_result_ids( + messages: list[dict[str, Any]], + start_index: int, +) -> set[str]: + ids: set[str] = set() + index = start_index + while index < len(messages) and messages[index].get('role') == 'tool': + tool_call_id = messages[index].get('tool_call_id') + if isinstance(tool_call_id, str) and tool_call_id: + ids.add(tool_call_id) + index += 1 + return ids + + +def _filter_tool_calls_by_ids( + message: dict[str, Any], + valid_ids: set[str], +) -> dict[str, Any]: + raw_tool_calls = message.get('tool_calls') + if not isinstance(raw_tool_calls, list): + return message + filtered: list[dict[str, Any]] = [] + for index, raw_call in enumerate(raw_tool_calls): + if not isinstance(raw_call, dict): + continue + call_id = raw_call.get('id') + normalized_id = ( + call_id if isinstance(call_id, str) and call_id else f'call_{index}' + ) + if normalized_id in valid_ids: + filtered.append(raw_call) + if len(filtered) == len(raw_tool_calls): + return message + next_message = dict(message) + if filtered: + next_message['tool_calls'] = filtered + else: + next_message.pop('tool_calls', None) + return next_message + + +def _sanitize_anthropic_tool_messages( + messages: list[dict[str, Any]], +) -> list[dict[str, Any]]: + """清理 Anthropic/Bedrock 严格要求的 tool_use/tool_result 邻接关系。 + + OpenAI 风格历史在异常中断、裁剪或前端恢复时可能留下 assistant tool_calls, + 但下一条消息不再紧跟对应 tool 结果。Anthropic Messages API 会直接拒绝 + 这种历史。这里在出网关前删除缺失结果的 tool_call,并跳过没有前置 + tool_use 的孤儿 tool 结果,保留普通文本上下文继续对话。 + """ + sanitized: list[dict[str, Any]] = [] + pending_tool_ids: set[str] = set() + + for index, message in enumerate(messages): + role = message.get('role') + if role == 'assistant': + tool_ids = _tool_call_ids(message) + if tool_ids: + result_ids = _contiguous_tool_result_ids(messages, index + 1) + valid_ids = set(tool_ids).intersection(result_ids) + message = _filter_tool_calls_by_ids(message, valid_ids) + pending_tool_ids = valid_ids + else: + pending_tool_ids = set() + sanitized.append(message) + continue + + if role == 'tool': + tool_call_id = message.get('tool_call_id') + if isinstance(tool_call_id, str) and tool_call_id in pending_tool_ids: + sanitized.append(message) + continue + + pending_tool_ids = set() + sanitized.append(message) + + return sanitized + + def _optional_int(value: Any) -> int: if isinstance(value, bool): return 0 @@ -341,7 +434,7 @@ class OpenAICompatClient: ) -> dict[str, Any]: system_parts: list[str] = [] anthropic_messages: list[dict[str, Any]] = [] - for message in messages: + for message in _sanitize_anthropic_tool_messages(messages): role = message.get('role') if role == 'system': content = _normalize_content(message.get('content')).strip()