mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-21 16:40:22 +00:00
## 背景 当 Claude Code 发送过多工具信息时,可能超出 Kiro API 请求限制导致 500 错误。 现有的工具描述截断(KiroMaxToolDescLen = 10237)只能限制单个工具的描述长度, 无法解决整体工具列表过大的问题。 ## 解决方案 实现动态工具压缩功能,采用两步压缩策略: 1. 先检查原始大小,超过 20KB 才进行压缩 2. 第一步:简化 input_schema,只保留 type/enum/required 字段 3. 第二步:按比例缩短 description(最短 50 字符) 4. 保留全部工具和 skills 可调用,不丢弃任何工具 ## 新增文件 - internal/translator/kiro/claude/tool_compression.go - calculateToolsSize(): 计算工具列表的 JSON 序列化大小 - simplifyInputSchema(): 简化 input_schema,递归处理嵌套 properties - compressToolDescription(): 按比例压缩描述,支持 UTF-8 安全截断 - compressToolsIfNeeded(): 主压缩函数,实现两步压缩策略 - internal/translator/kiro/claude/tool_compression_test.go - 完整的单元测试覆盖所有新增函数 - 测试 UTF-8 安全性 - 测试压缩效果 ## 修改文件 - internal/translator/kiro/common/constants.go - 新增 ToolCompressionTargetSize = 20KB (压缩目标大小阈值) - 新增 MinToolDescriptionLength = 50 (描述最短长度) - internal/translator/kiro/claude/kiro_claude_request.go - 在 convertClaudeToolsToKiro() 函数末尾调用 compressToolsIfNeeded() ## 测试结果 - 70KB 工具压缩至 17KB (74.7% 压缩率) - 所有单元测试通过 ## 预期效果 - 80KB+ tools 压缩至 ~15KB - 不影响工具调用功能
198 lines
6.5 KiB
Go
198 lines
6.5 KiB
Go
// 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 "..."
|
|
if truncLen < kirocommon.MinToolDescriptionLength-3 {
|
|
truncLen = kirocommon.MinToolDescriptionLength - 3
|
|
}
|
|
|
|
// 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
|
|
// Calculate the compression ratio needed
|
|
compressionRatio := float64(kirocommon.ToolCompressionTargetSize) / float64(sizeAfterSchemaSimplification)
|
|
if compressionRatio > 1.0 {
|
|
compressionRatio = 1.0
|
|
}
|
|
|
|
// Calculate total description length and target
|
|
totalDescLen := 0
|
|
for _, tool := range compressedTools {
|
|
totalDescLen += len(tool.ToolSpecification.Description)
|
|
}
|
|
|
|
// Estimate how much we need to reduce descriptions
|
|
// Assume descriptions account for roughly 50% of the payload
|
|
targetDescRatio := compressionRatio * 0.8 // Be more aggressive with description compression
|
|
|
|
for i := range compressedTools {
|
|
desc := compressedTools[i].ToolSpecification.Description
|
|
targetLen := int(float64(len(desc)) * targetDescRatio)
|
|
if targetLen < kirocommon.MinToolDescriptionLength {
|
|
targetLen = kirocommon.MinToolDescriptionLength
|
|
}
|
|
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
|
|
}
|