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..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 } @@ -342,7 +347,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 = ""