fix(bedrock): sanitize tool names for Bedrock API constraints#1474
fix(bedrock): sanitize tool names for Bedrock API constraints#1474frank-bee wants to merge 4 commits intokagent-dev:mainfrom
Conversation
Bedrock requires tool names to match [a-zA-Z0-9_-]+ with length >= 1. MCP servers (e.g. GitHub Copilot) may produce tool names with dots, spaces, or other invalid characters. Override generate_content_async in KAgentLiteLlm to sanitize tool names in both the outgoing request history and incoming response parts when using a Bedrock model. Signed-off-by: Frank Bernhardt <Frank.Bernhardt@quantum-machines.co>
b83598f to
7555d12
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates the KAgentLiteLlm Bedrock integration to prevent Amazon Bedrock API validation errors caused by tool names containing disallowed characters.
Changes:
- Add Bedrock tool-name validation/sanitization helpers (
_sanitize_tool_name, request/response sanitizers). - Override
generate_content_asyncto sanitize function-call tool names in outgoing request history and incoming responses when targeting Bedrock.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async def generate_content_async( | ||
| self, llm_request: LlmRequest, stream: bool = False | ||
| ) -> AsyncGenerator[LlmResponse, None]: | ||
| effective_model = llm_request.model or self.model | ||
| if _is_bedrock_model(effective_model): | ||
| _sanitize_llm_request(llm_request) | ||
| idx = 0 | ||
| async for response in super().generate_content_async(llm_request, stream=stream): | ||
| yield _sanitize_llm_response(response, idx) | ||
| idx += 1 |
There was a problem hiding this comment.
Tool-name sanitization logs at WARNING every time an invalid name is seen. In streaming mode, generate_content_async yields many chunks, so the same invalid tool name could generate a large number of warning log lines (one per chunk/part), which can be noisy and increase log volume. Consider de-duplicating warnings per request/response (e.g., track already-sanitized originals) or lowering to INFO/DEBUG if this is expected in normal operation.
There was a problem hiding this comment.
I agree with this, I think DEBUG if at all.
| async def generate_content_async( | ||
| self, llm_request: LlmRequest, stream: bool = False | ||
| ) -> AsyncGenerator[LlmResponse, None]: | ||
| effective_model = llm_request.model or self.model | ||
| if _is_bedrock_model(effective_model): | ||
| _sanitize_llm_request(llm_request) | ||
| idx = 0 | ||
| async for response in super().generate_content_async(llm_request, stream=stream): | ||
| yield _sanitize_llm_response(response, idx) | ||
| idx += 1 | ||
| else: | ||
| async for response in super().generate_content_async(llm_request, stream=stream): | ||
| yield response |
There was a problem hiding this comment.
This change introduces Bedrock-specific mutation of LlmRequest/LlmResponse tool-call names, but there are no unit tests exercising the new sanitization logic. Adding focused tests (e.g., construct an LlmRequest with function_call parts containing invalid names and assert they are sanitized when model is bedrock/..., and unchanged otherwise) would help prevent regressions.
There was a problem hiding this comment.
Agreed, we need unit tests
There was a problem hiding this comment.
Added unit tests in tests/unittests/models/test_litellm_bedrock.py (15 tests, all passing locally). Covers sanitize helpers, Bedrock vs non-Bedrock routing, no-collision across parts, and log-level assertion.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
| async def generate_content_async( | ||
| self, llm_request: LlmRequest, stream: bool = False | ||
| ) -> AsyncGenerator[LlmResponse, None]: | ||
| effective_model = llm_request.model or self.model | ||
| if _is_bedrock_model(effective_model): | ||
| _sanitize_llm_request(llm_request) | ||
| idx = 0 | ||
| async for response in super().generate_content_async(llm_request, stream=stream): | ||
| yield _sanitize_llm_response(response, idx) | ||
| idx += 1 |
There was a problem hiding this comment.
I agree with this, I think DEBUG if at all.
| async def generate_content_async( | ||
| self, llm_request: LlmRequest, stream: bool = False | ||
| ) -> AsyncGenerator[LlmResponse, None]: | ||
| effective_model = llm_request.model or self.model | ||
| if _is_bedrock_model(effective_model): | ||
| _sanitize_llm_request(llm_request) | ||
| idx = 0 | ||
| async for response in super().generate_content_async(llm_request, stream=stream): | ||
| yield _sanitize_llm_response(response, idx) | ||
| idx += 1 | ||
| else: | ||
| async for response in super().generate_content_async(llm_request, stream=stream): | ||
| yield response |
There was a problem hiding this comment.
Agreed, we need unit tests
- Change logger.warning -> logger.debug for tool-name sanitization to avoid log noise in streaming mode - Fix _sanitize_tool_name type hint to accept str|int (composite suffix) - Add unit tests covering sanitize helpers and Bedrock vs non-Bedrock routing Signed-off-by: Frank Bernhardt <Frank.Bernhardt@quantum-machines.co>
f1a2e39 to
bc1b190
Compare
|
Addressed all review comments in bc1b190:
All 15 tests pass locally. |
Fixes #1473
Problem
When using kagent with Amazon Bedrock models, MCP servers (e.g. GitHub Copilot) may produce tool names containing dots, spaces, or other characters that violate Bedrock's API constraints.
Bedrock requires tool names to match
[a-zA-Z0-9_-]+with a minimum length of 1. Any name outside this pattern causes API errors like:Root Cause
KAgentLiteLlmdoes not sanitize tool names before passing them to the Bedrock API, and does not sanitize tool names in the conversation history (function call parts) sent in subsequent requests.Fix
Override
generate_content_asyncinKAgentLiteLlmto:LlmRequesthistory (function call parts incontents)LlmResponsepartsInvalid characters are replaced with
_. Empty/completely invalid names fall back tounknown_tool_<idx>. A warning is logged whenever a name is sanitized, to aid debugging.