From c51851689b50fcbfb2cba32ef1e33c3a751ee930 Mon Sep 17 00:00:00 2001 From: CheesesNguyen Date: Thu, 5 Mar 2026 10:05:39 +0700 Subject: [PATCH 1/2] fix: remove SOFT_LIMIT_REACHED logic, tool compression, and fix bugs - Remove SOFT_LIMIT_REACHED marker injection in response path - Remove SOFT_LIMIT_REACHED detection logic in request path - Remove SOFT_LIMIT_REACHED streaming logic in executor - Remove tool_compression.go and related constants - Fix truncation_detector: string(rune(len)) producing Unicode char instead of decimal string - Fix WebSearchToolUseId being overwritten by non-web-search tools - Fix duplicate kiro entry in model_definitions.go comment - Add build output to .gitignore --- .gitignore | 4 + internal/registry/model_definitions.go | 1 - internal/runtime/executor/kiro_executor.go | 60 +----- .../kiro/claude/kiro_claude_request.go | 33 +-- .../kiro/claude/kiro_claude_response.go | 39 +--- .../kiro/claude/kiro_claude_stream_parser.go | 4 +- .../kiro/claude/tool_compression.go | 191 ------------------ .../kiro/claude/truncation_detector.go | 2 +- internal/translator/kiro/common/constants.go | 8 - 9 files changed, 19 insertions(+), 323 deletions(-) delete mode 100644 internal/translator/kiro/claude/tool_compression.go diff --git a/.gitignore b/.gitignore index e6e6ab0a..51443e42 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,7 @@ _bmad-output/* .DS_Store ._* *.bak + +# Build output +CLIProxyAPIPlus +CLIProxyAPIPlus.app/ diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index 802ce290..cddfc2bb 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -23,7 +23,6 @@ import ( // - kiro // - kilo // - github-copilot -// - kiro // - amazonq // - antigravity (returns static overrides only) func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo { diff --git a/internal/runtime/executor/kiro_executor.go b/internal/runtime/executor/kiro_executor.go index f5e5d9ae..5aaf3b97 100644 --- a/internal/runtime/executor/kiro_executor.go +++ b/internal/runtime/executor/kiro_executor.go @@ -2458,7 +2458,6 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out reader := bufio.NewReaderSize(body, 20*1024*1024) // 20MB buffer to match other providers var totalUsage usage.Detail var hasToolUses bool // Track if any tool uses were emitted - var hasTruncatedTools bool // Track if any tool uses were truncated var upstreamStopReason string // Track stop_reason from upstream events // Tool use state tracking for input buffering and deduplication @@ -3286,59 +3285,9 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Emit completed tool uses for _, tu := range completedToolUses { - // Check if this tool was truncated - emit with SOFT_LIMIT_REACHED marker + // Skip truncated tools - don't emit fake marker tool_use if tu.IsTruncated { - hasTruncatedTools = true - log.Infof("kiro: streamToChannel emitting truncated tool with SOFT_LIMIT_REACHED: %s (ID: %s)", tu.Name, tu.ToolUseID) - - // Close text block if open - if isTextBlockOpen && contentBlockIndex >= 0 { - blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) - for _, chunk := range sseData { - if chunk != "" { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} - } - } - isTextBlockOpen = false - } - - contentBlockIndex++ - - // Emit tool_use with SOFT_LIMIT_REACHED marker input - blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", tu.ToolUseID, tu.Name) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam) - for _, chunk := range sseData { - if chunk != "" { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} - } - } - - // Build SOFT_LIMIT_REACHED marker input - markerInput := map[string]interface{}{ - "_status": "SOFT_LIMIT_REACHED", - "_message": "Tool output was truncated. Split content into smaller chunks (max 300 lines). Due to potential model hallucination, you MUST re-fetch the current working directory and generate the correct file_path.", - } - - markerJSON, _ := json.Marshal(markerInput) - inputDelta := kiroclaude.BuildClaudeInputJsonDeltaEvent(string(markerJSON), contentBlockIndex) - sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam) - for _, chunk := range sseData { - if chunk != "" { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} - } - } - - // Close tool_use block - blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex) - sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) - for _, chunk := range sseData { - if chunk != "" { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} - } - } - - hasToolUses = true // Keep this so stop_reason = tool_use + log.Warnf("kiro: streamToChannel skipping truncated tool: %s (ID: %s)", tu.Name, tu.ToolUseID) continue } @@ -3640,12 +3589,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out } // Determine stop reason: prefer upstream, then detect tool_use, default to end_turn - // SOFT_LIMIT_REACHED: Keep stop_reason = "tool_use" so Claude continues the loop stopReason := upstreamStopReason - if hasTruncatedTools { - // Log that we're using SOFT_LIMIT_REACHED approach - log.Infof("kiro: streamToChannel using SOFT_LIMIT_REACHED - keeping stop_reason=tool_use for truncated tools") - } if stopReason == "" { if hasToolUses { stopReason = "tool_use" diff --git a/internal/translator/kiro/claude/kiro_claude_request.go b/internal/translator/kiro/claude/kiro_claude_request.go index 067a2710..ad140b94 100644 --- a/internal/translator/kiro/claude/kiro_claude_request.go +++ b/internal/translator/kiro/claude/kiro_claude_request.go @@ -605,10 +605,6 @@ 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 } @@ -858,34 +854,7 @@ func BuildUserMessageStruct(msg gjson.Result, modelID, origin string) (KiroUserI var textContents []KiroTextContent - // Check if this tool_result contains error from our SOFT_LIMIT_REACHED tool_use - // The client will return an error when trying to execute a tool with marker input - resultStr := resultContent.String() - isSoftLimitError := strings.Contains(resultStr, "SOFT_LIMIT_REACHED") || - strings.Contains(resultStr, "_status") || - strings.Contains(resultStr, "truncated") || - strings.Contains(resultStr, "missing required") || - strings.Contains(resultStr, "invalid input") || - strings.Contains(resultStr, "Error writing file") - - if isError && isSoftLimitError { - // Replace error content with SOFT_LIMIT_REACHED guidance - log.Infof("kiro: detected SOFT_LIMIT_REACHED in tool_result for %s, replacing with guidance", toolUseID) - softLimitMsg := `SOFT_LIMIT_REACHED - -Your previous tool call was incomplete due to API output size limits. -The content was PARTIALLY transmitted but NOT executed. - -REQUIRED ACTION: -1. Split your content into smaller chunks (max 300 lines per call) -2. For file writes: Create file with first chunk, then use append for remaining -3. Do NOT regenerate content you already attempted - continue from where you stopped - -STATUS: This is NOT an error. Continue with smaller chunks.` - textContents = append(textContents, KiroTextContent{Text: softLimitMsg}) - // Mark as SUCCESS so Claude doesn't treat it as a failure - isError = false - } else if resultContent.IsArray() { + if resultContent.IsArray() { for _, item := range resultContent.Array() { if item.Get("type").String() == "text" { textContents = append(textContents, KiroTextContent{Text: item.Get("text").String()}) diff --git a/internal/translator/kiro/claude/kiro_claude_response.go b/internal/translator/kiro/claude/kiro_claude_response.go index 89a760cd..17ffdde2 100644 --- a/internal/translator/kiro/claude/kiro_claude_response.go +++ b/internal/translator/kiro/claude/kiro_claude_response.go @@ -55,39 +55,18 @@ func BuildClaudeResponse(content string, toolUses []KiroToolUse, model string, u } } - // Add tool_use blocks - emit truncated tools with SOFT_LIMIT_REACHED marker - hasTruncatedTools := false + // Add tool_use blocks - skip truncated tools and log warning for _, toolUse := range toolUses { if toolUse.IsTruncated && toolUse.TruncationInfo != nil { - // Emit tool_use with SOFT_LIMIT_REACHED marker input - hasTruncatedTools = true - log.Infof("kiro: buildClaudeResponse emitting truncated tool with SOFT_LIMIT_REACHED: %s (ID: %s)", toolUse.Name, toolUse.ToolUseID) - - markerInput := map[string]interface{}{ - "_status": "SOFT_LIMIT_REACHED", - "_message": "Tool output was truncated. Split content into smaller chunks (max 300 lines). Due to potential model hallucination, you MUST re-fetch the current working directory and generate the correct file_path.", - } - - contentBlocks = append(contentBlocks, map[string]interface{}{ - "type": "tool_use", - "id": toolUse.ToolUseID, - "name": toolUse.Name, - "input": markerInput, - }) - } else { - // Normal tool use - contentBlocks = append(contentBlocks, map[string]interface{}{ - "type": "tool_use", - "id": toolUse.ToolUseID, - "name": toolUse.Name, - "input": toolUse.Input, - }) + log.Warnf("kiro: buildClaudeResponse skipping truncated tool: %s (ID: %s)", toolUse.Name, toolUse.ToolUseID) + continue } - } - - // Log if we used SOFT_LIMIT_REACHED - if hasTruncatedTools { - log.Infof("kiro: buildClaudeResponse using SOFT_LIMIT_REACHED - keeping stop_reason=tool_use") + contentBlocks = append(contentBlocks, map[string]interface{}{ + "type": "tool_use", + "id": toolUse.ToolUseID, + "name": toolUse.Name, + "input": toolUse.Input, + }) } // Ensure at least one content block (Claude API requires non-empty content) diff --git a/internal/translator/kiro/claude/kiro_claude_stream_parser.go b/internal/translator/kiro/claude/kiro_claude_stream_parser.go index 275196ac..03422072 100644 --- a/internal/translator/kiro/claude/kiro_claude_stream_parser.go +++ b/internal/translator/kiro/claude/kiro_claude_stream_parser.go @@ -192,8 +192,8 @@ func AnalyzeBufferedStream(chunks [][]byte) BufferedStreamResult { if idx, ok := event["index"].(float64); ok { currentToolIndex = int(idx) } - // Capture tool use ID for toolResults handshake - if id, ok := cb["id"].(string); ok { + // Capture tool use ID only for web_search toolResults handshake + if id, ok := cb["id"].(string); ok && (currentToolName == "web_search" || currentToolName == "remote_web_search") { result.WebSearchToolUseId = id } toolInputBuilder.Reset() diff --git a/internal/translator/kiro/claude/tool_compression.go b/internal/translator/kiro/claude/tool_compression.go deleted file mode 100644 index 7d4a424e..00000000 --- a/internal/translator/kiro/claude/tool_compression.go +++ /dev/null @@ -1,191 +0,0 @@ -// 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/claude/truncation_detector.go b/internal/translator/kiro/claude/truncation_detector.go index 056c6702..7a72431b 100644 --- a/internal/translator/kiro/claude/truncation_detector.go +++ b/internal/translator/kiro/claude/truncation_detector.go @@ -342,7 +342,7 @@ func buildTruncationErrorMessage(toolName, truncationType string, parsedFields m } sb.WriteString(" Received ") - sb.WriteString(string(rune(len(rawInput)))) + sb.WriteString(formatInt(len(rawInput))) sb.WriteString(" bytes. Please retry with smaller content chunks.") return sb.String() diff --git a/internal/translator/kiro/common/constants.go b/internal/translator/kiro/common/constants.go index 3016947c..a7c21e6e 100644 --- a/internal/translator/kiro/common/constants.go +++ b/internal/translator/kiro/common/constants.go @@ -6,14 +6,6 @@ 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 = "" From 7fe1d102cbe15c041cda46abc8301abd7efbb747 Mon Sep 17 00:00:00 2001 From: CheesesNguyen Date: Thu, 5 Mar 2026 14:43:45 +0700 Subject: [PATCH 2/2] fix: don't treat empty input as truncation for tools without required fields Tools like TaskList, TaskGet have no required parameters, so empty input is valid. Previously, the truncation detector flagged all empty inputs as truncated, causing these tools to be skipped and breaking the tool loop. Now only flag empty input as truncation when the tool has required fields defined in RequiredFieldsByTool. --- .../kiro/claude/truncation_detector.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/translator/kiro/claude/truncation_detector.go b/internal/translator/kiro/claude/truncation_detector.go index 7a72431b..cc0d3484 100644 --- a/internal/translator/kiro/claude/truncation_detector.go +++ b/internal/translator/kiro/claude/truncation_detector.go @@ -84,13 +84,18 @@ func DetectTruncation(toolName, toolUseID, rawInput string, parsedInput map[stri ParsedFields: make(map[string]string), } - // Scenario 1: Empty input buffer - no data received at all + // Scenario 1: Empty input buffer - only flag as truncation if tool has required fields + // Many tools (e.g. TaskList, TaskGet) have no required params, so empty input is valid if strings.TrimSpace(rawInput) == "" { - info.IsTruncated = true - info.TruncationType = TruncationTypeEmptyInput - info.ErrorMessage = "Tool input was completely empty - API response may have been truncated before tool parameters were transmitted" - log.Warnf("kiro: truncation detected [%s] for tool %s (ID: %s): empty input buffer", - info.TruncationType, toolName, toolUseID) + if _, hasRequirements := RequiredFieldsByTool[toolName]; hasRequirements { + info.IsTruncated = true + info.TruncationType = TruncationTypeEmptyInput + info.ErrorMessage = "Tool input was completely empty - API response may have been truncated before tool parameters were transmitted" + log.Warnf("kiro: truncation detected [%s] for tool %s (ID: %s): empty input buffer", + info.TruncationType, toolName, toolUseID) + return info + } + log.Debugf("kiro: empty input for tool %s (ID: %s) - no required fields, treating as valid", toolName, toolUseID) return info }