mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-09 15:25:17 +00:00
@@ -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
|
||||
}
|
||||
|
||||
|
||||
191
internal/translator/kiro/claude/tool_compression.go
Normal file
191
internal/translator/kiro/claude/tool_compression.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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 = "<thinking>"
|
||||
|
||||
@@ -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.`
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user