diff --git a/internal/translator/kiro/claude/kiro_claude_request.go b/internal/translator/kiro/claude/kiro_claude_request.go index 402591e7..06141a29 100644 --- a/internal/translator/kiro/claude/kiro_claude_request.go +++ b/internal/translator/kiro/claude/kiro_claude_request.go @@ -520,7 +520,7 @@ func convertClaudeToolsToKiro(tools gjson.Result) []KiroToolWrapper { log.Debugf("kiro: tool '%s' has empty description, using default: %s", name, description) } - // Truncate long descriptions + // Truncate long descriptions (individual tool limit) if len(description) > kirocommon.KiroMaxToolDescLen { truncLen := kirocommon.KiroMaxToolDescLen - 30 for truncLen > 0 && !utf8.RuneStart(description[truncLen]) { @@ -538,6 +538,10 @@ func convertClaudeToolsToKiro(tools gjson.Result) []KiroToolWrapper { }) } + // Apply dynamic compression if total tools size exceeds threshold + // This prevents 500 errors when Claude Code sends too many tools + kiroTools = compressToolsIfNeeded(kiroTools) + return kiroTools } diff --git a/internal/translator/kiro/claude/tool_compression.go b/internal/translator/kiro/claude/tool_compression.go new file mode 100644 index 00000000..7d4a424e --- /dev/null +++ b/internal/translator/kiro/claude/tool_compression.go @@ -0,0 +1,191 @@ +// Package claude provides tool compression functionality for Kiro translator. +// This file implements dynamic tool compression to reduce tool payload size +// when it exceeds the target threshold, preventing 500 errors from Kiro API. +package claude + +import ( + "encoding/json" + "unicode/utf8" + + kirocommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/kiro/common" + log "github.com/sirupsen/logrus" +) + +// calculateToolsSize calculates the JSON serialized size of the tools list. +// Returns the size in bytes. +func calculateToolsSize(tools []KiroToolWrapper) int { + if len(tools) == 0 { + return 0 + } + data, err := json.Marshal(tools) + if err != nil { + log.Warnf("kiro: failed to marshal tools for size calculation: %v", err) + return 0 + } + return len(data) +} + +// simplifyInputSchema simplifies the input_schema by keeping only essential fields: +// type, enum, required. Recursively processes nested properties. +func simplifyInputSchema(schema interface{}) interface{} { + if schema == nil { + return nil + } + + schemaMap, ok := schema.(map[string]interface{}) + if !ok { + return schema + } + + simplified := make(map[string]interface{}) + + // Keep essential fields + if t, ok := schemaMap["type"]; ok { + simplified["type"] = t + } + if enum, ok := schemaMap["enum"]; ok { + simplified["enum"] = enum + } + if required, ok := schemaMap["required"]; ok { + simplified["required"] = required + } + + // Recursively process properties + if properties, ok := schemaMap["properties"].(map[string]interface{}); ok { + simplifiedProps := make(map[string]interface{}) + for key, value := range properties { + simplifiedProps[key] = simplifyInputSchema(value) + } + simplified["properties"] = simplifiedProps + } + + // Process items for array types + if items, ok := schemaMap["items"]; ok { + simplified["items"] = simplifyInputSchema(items) + } + + // Process additionalProperties if present + if additionalProps, ok := schemaMap["additionalProperties"]; ok { + simplified["additionalProperties"] = simplifyInputSchema(additionalProps) + } + + // Process anyOf, oneOf, allOf + for _, key := range []string{"anyOf", "oneOf", "allOf"} { + if arr, ok := schemaMap[key].([]interface{}); ok { + simplifiedArr := make([]interface{}, len(arr)) + for i, item := range arr { + simplifiedArr[i] = simplifyInputSchema(item) + } + simplified[key] = simplifiedArr + } + } + + return simplified +} + +// compressToolDescription compresses a description to the target length. +// Ensures the result is at least MinToolDescriptionLength characters. +// Uses UTF-8 safe truncation. +func compressToolDescription(description string, targetLength int) string { + if targetLength < kirocommon.MinToolDescriptionLength { + targetLength = kirocommon.MinToolDescriptionLength + } + + if len(description) <= targetLength { + return description + } + + // Find a safe truncation point (UTF-8 boundary) + truncLen := targetLength - 3 // Leave room for "..." + + // Ensure we don't cut in the middle of a UTF-8 character + for truncLen > 0 && !utf8.RuneStart(description[truncLen]) { + truncLen-- + } + + if truncLen <= 0 { + return description[:kirocommon.MinToolDescriptionLength] + } + + return description[:truncLen] + "..." +} + +// compressToolsIfNeeded compresses tools if their total size exceeds the target threshold. +// Compression strategy: +// 1. First, check if compression is needed (size > ToolCompressionTargetSize) +// 2. Step 1: Simplify input_schema (keep only type/enum/required) +// 3. Step 2: Proportionally compress descriptions (minimum MinToolDescriptionLength chars) +// Returns the compressed tools list. +func compressToolsIfNeeded(tools []KiroToolWrapper) []KiroToolWrapper { + if len(tools) == 0 { + return tools + } + + originalSize := calculateToolsSize(tools) + if originalSize <= kirocommon.ToolCompressionTargetSize { + log.Debugf("kiro: tools size %d bytes is within target %d bytes, no compression needed", + originalSize, kirocommon.ToolCompressionTargetSize) + return tools + } + + log.Infof("kiro: tools size %d bytes exceeds target %d bytes, starting compression", + originalSize, kirocommon.ToolCompressionTargetSize) + + // Create a copy of tools to avoid modifying the original + compressedTools := make([]KiroToolWrapper, len(tools)) + for i, tool := range tools { + compressedTools[i] = KiroToolWrapper{ + ToolSpecification: KiroToolSpecification{ + Name: tool.ToolSpecification.Name, + Description: tool.ToolSpecification.Description, + InputSchema: KiroInputSchema{JSON: tool.ToolSpecification.InputSchema.JSON}, + }, + } + } + + // Step 1: Simplify input_schema + for i := range compressedTools { + compressedTools[i].ToolSpecification.InputSchema.JSON = + simplifyInputSchema(compressedTools[i].ToolSpecification.InputSchema.JSON) + } + + sizeAfterSchemaSimplification := calculateToolsSize(compressedTools) + log.Debugf("kiro: size after schema simplification: %d bytes (reduced by %d bytes)", + sizeAfterSchemaSimplification, originalSize-sizeAfterSchemaSimplification) + + // Check if we're within target after schema simplification + if sizeAfterSchemaSimplification <= kirocommon.ToolCompressionTargetSize { + log.Infof("kiro: compression complete after schema simplification, final size: %d bytes", + sizeAfterSchemaSimplification) + return compressedTools + } + + // Step 2: Compress descriptions proportionally + sizeToReduce := float64(sizeAfterSchemaSimplification - kirocommon.ToolCompressionTargetSize) + var totalDescLen float64 + for _, tool := range compressedTools { + totalDescLen += float64(len(tool.ToolSpecification.Description)) + } + + if totalDescLen > 0 { + // Assume size reduction comes primarily from descriptions. + keepRatio := 1.0 - (sizeToReduce / totalDescLen) + if keepRatio > 1.0 { + keepRatio = 1.0 + } else if keepRatio < 0 { + keepRatio = 0 + } + + for i := range compressedTools { + desc := compressedTools[i].ToolSpecification.Description + targetLen := int(float64(len(desc)) * keepRatio) + compressedTools[i].ToolSpecification.Description = compressToolDescription(desc, targetLen) + } + } + + finalSize := calculateToolsSize(compressedTools) + log.Infof("kiro: compression complete, original: %d bytes, final: %d bytes (%.1f%% reduction)", + originalSize, finalSize, float64(originalSize-finalSize)/float64(originalSize)*100) + + return compressedTools +} diff --git a/internal/translator/kiro/common/constants.go b/internal/translator/kiro/common/constants.go index 96174b8c..2327ab59 100644 --- a/internal/translator/kiro/common/constants.go +++ b/internal/translator/kiro/common/constants.go @@ -6,6 +6,14 @@ const ( // Kiro API limit is 10240 bytes, leave room for "..." KiroMaxToolDescLen = 10237 + // ToolCompressionTargetSize is the target total size for compressed tools (20KB). + // If tools exceed this size, compression will be applied. + ToolCompressionTargetSize = 20 * 1024 // 20KB + + // MinToolDescriptionLength is the minimum description length after compression. + // Descriptions will not be shortened below this length. + MinToolDescriptionLength = 50 + // ThinkingStartTag is the start tag for thinking blocks in responses. ThinkingStartTag = "" @@ -72,4 +80,4 @@ You MUST follow these rules for ALL file operations. Violation causes server tim - Failed writes waste time and require retry REMEMBER: When in doubt, write LESS per operation. Multiple small operations > one large operation.` -) \ No newline at end of file +)