From 5d716dc796b92756b895dab5e88ea8afaacb687e Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 7 Dec 2025 21:34:44 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix(kiro):=20=E4=BF=AE=E5=A4=8D=20base64=20?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=A0=BC=E5=BC=8F=E8=BD=AC=E6=8D=A2=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat-completions/kiro_openai_request.go | 89 ++++++++++++++++--- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/internal/translator/kiro/openai/chat-completions/kiro_openai_request.go b/internal/translator/kiro/openai/chat-completions/kiro_openai_request.go index fc850d96..2dbf79b9 100644 --- a/internal/translator/kiro/openai/chat-completions/kiro_openai_request.go +++ b/internal/translator/kiro/openai/chat-completions/kiro_openai_request.go @@ -4,6 +4,7 @@ package chat_completions import ( "bytes" "encoding/json" + "strings" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -182,13 +183,43 @@ func ConvertOpenAIRequestToKiro(modelName string, inputRawJSON []byte, stream bo "text": part.Get("text").String(), }) } else if partType == "image_url" { - contentParts = append(contentParts, map[string]interface{}{ - "type": "image", - "source": map[string]interface{}{ - "type": "url", - "url": part.Get("image_url.url").String(), - }, - }) + imageURL := part.Get("image_url.url").String() + + // 检查是否是base64格式 (data:image/png;base64,xxxxx) + if strings.HasPrefix(imageURL, "data:") { + // 解析 data URL 格式 + // 格式: data:image/png;base64,xxxxx + commaIdx := strings.Index(imageURL, ",") + if commaIdx != -1 { + // 提取 media_type (例如 "image/png") + header := imageURL[5:commaIdx] // 去掉 "data:" 前缀 + mediaType := header + if semiIdx := strings.Index(header, ";"); semiIdx != -1 { + mediaType = header[:semiIdx] + } + + // 提取 base64 数据 + base64Data := imageURL[commaIdx+1:] + + contentParts = append(contentParts, map[string]interface{}{ + "type": "image", + "source": map[string]interface{}{ + "type": "base64", + "media_type": mediaType, + "data": base64Data, + }, + }) + } + } else { + // 普通URL格式 - 保持原有逻辑 + contentParts = append(contentParts, map[string]interface{}{ + "type": "image", + "source": map[string]interface{}{ + "type": "url", + "url": imageURL, + }, + }) + } } } } else if content.String() != "" { @@ -214,13 +245,43 @@ func ConvertOpenAIRequestToKiro(modelName string, inputRawJSON []byte, stream bo "text": part.Get("text").String(), }) } else if partType == "image_url" { - contentParts = append(contentParts, map[string]interface{}{ - "type": "image", - "source": map[string]interface{}{ - "type": "url", - "url": part.Get("image_url.url").String(), - }, - }) + imageURL := part.Get("image_url.url").String() + + // 检查是否是base64格式 (data:image/png;base64,xxxxx) + if strings.HasPrefix(imageURL, "data:") { + // 解析 data URL 格式 + // 格式: data:image/png;base64,xxxxx + commaIdx := strings.Index(imageURL, ",") + if commaIdx != -1 { + // 提取 media_type (例如 "image/png") + header := imageURL[5:commaIdx] // 去掉 "data:" 前缀 + mediaType := header + if semiIdx := strings.Index(header, ";"); semiIdx != -1 { + mediaType = header[:semiIdx] + } + + // 提取 base64 数据 + base64Data := imageURL[commaIdx+1:] + + contentParts = append(contentParts, map[string]interface{}{ + "type": "image", + "source": map[string]interface{}{ + "type": "base64", + "media_type": mediaType, + "data": base64Data, + }, + }) + } + } else { + // 普通URL格式 - 保持原有逻辑 + contentParts = append(contentParts, map[string]interface{}{ + "type": "image", + "source": map[string]interface{}{ + "type": "url", + "url": imageURL, + }, + }) + } } } claudeMsg["content"] = contentParts From 2bf9e08b31f11b718c3e24c2eda6dbf80677f159 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 7 Dec 2025 21:50:06 +0800 Subject: [PATCH 2/4] style(kiro): convert Chinese comments to English in base64 image handling --- .../chat-completions/kiro_openai_request.go | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/translator/kiro/openai/chat-completions/kiro_openai_request.go b/internal/translator/kiro/openai/chat-completions/kiro_openai_request.go index 2dbf79b9..3d339505 100644 --- a/internal/translator/kiro/openai/chat-completions/kiro_openai_request.go +++ b/internal/translator/kiro/openai/chat-completions/kiro_openai_request.go @@ -185,20 +185,20 @@ func ConvertOpenAIRequestToKiro(modelName string, inputRawJSON []byte, stream bo } else if partType == "image_url" { imageURL := part.Get("image_url.url").String() - // 检查是否是base64格式 (data:image/png;base64,xxxxx) + // Check if it's base64 format (data:image/png;base64,xxxxx) if strings.HasPrefix(imageURL, "data:") { - // 解析 data URL 格式 - // 格式: data:image/png;base64,xxxxx + // Parse data URL format + // Format: data:image/png;base64,xxxxx commaIdx := strings.Index(imageURL, ",") if commaIdx != -1 { - // 提取 media_type (例如 "image/png") - header := imageURL[5:commaIdx] // 去掉 "data:" 前缀 + // Extract media_type (e.g., "image/png") + header := imageURL[5:commaIdx] // Remove "data:" prefix mediaType := header if semiIdx := strings.Index(header, ";"); semiIdx != -1 { mediaType = header[:semiIdx] } - // 提取 base64 数据 + // Extract base64 data base64Data := imageURL[commaIdx+1:] contentParts = append(contentParts, map[string]interface{}{ @@ -211,7 +211,7 @@ func ConvertOpenAIRequestToKiro(modelName string, inputRawJSON []byte, stream bo }) } } else { - // 普通URL格式 - 保持原有逻辑 + // Regular URL format - keep original logic contentParts = append(contentParts, map[string]interface{}{ "type": "image", "source": map[string]interface{}{ @@ -247,20 +247,20 @@ func ConvertOpenAIRequestToKiro(modelName string, inputRawJSON []byte, stream bo } else if partType == "image_url" { imageURL := part.Get("image_url.url").String() - // 检查是否是base64格式 (data:image/png;base64,xxxxx) + // Check if it's base64 format (data:image/png;base64,xxxxx) if strings.HasPrefix(imageURL, "data:") { - // 解析 data URL 格式 - // 格式: data:image/png;base64,xxxxx + // Parse data URL format + // Format: data:image/png;base64,xxxxx commaIdx := strings.Index(imageURL, ",") if commaIdx != -1 { - // 提取 media_type (例如 "image/png") - header := imageURL[5:commaIdx] // 去掉 "data:" 前缀 + // Extract media_type (e.g., "image/png") + header := imageURL[5:commaIdx] // Remove "data:" prefix mediaType := header if semiIdx := strings.Index(header, ";"); semiIdx != -1 { mediaType = header[:semiIdx] } - // 提取 base64 数据 + // Extract base64 data base64Data := imageURL[commaIdx+1:] contentParts = append(contentParts, map[string]interface{}{ @@ -273,7 +273,7 @@ func ConvertOpenAIRequestToKiro(modelName string, inputRawJSON []byte, stream bo }) } } else { - // 普通URL格式 - 保持原有逻辑 + // Regular URL format - keep original logic contentParts = append(contentParts, map[string]interface{}{ "type": "image", "source": map[string]interface{}{ From 084e2666cb261f0fc0c2ee92e0f38bbb6e0e5e97 Mon Sep 17 00:00:00 2001 From: Ravens Date: Thu, 11 Dec 2025 00:13:44 +0800 Subject: [PATCH 3/4] fix(kiro): add SSE event: prefix for Claude client compatibility Amp-Thread-ID: https://ampcode.com/threads/T-019b08fc-ff96-766e-a942-63dd35ed28c6 Co-authored-by: Amp --- .../translator/kiro/claude/kiro_claude.go | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/internal/translator/kiro/claude/kiro_claude.go b/internal/translator/kiro/claude/kiro_claude.go index 9922860e..335873a7 100644 --- a/internal/translator/kiro/claude/kiro_claude.go +++ b/internal/translator/kiro/claude/kiro_claude.go @@ -1,10 +1,14 @@ // Package claude provides translation between Kiro and Claude formats. // Since Kiro uses Claude-compatible format internally, translations are mostly pass-through. +// However, SSE events require proper "event: " prefix for Claude clients. package claude import ( "bytes" "context" + "strings" + + "github.com/tidwall/gjson" ) // ConvertClaudeRequestToKiro converts Claude request to Kiro format. @@ -14,8 +18,52 @@ func ConvertClaudeRequestToKiro(modelName string, inputRawJSON []byte, stream bo } // ConvertKiroResponseToClaude converts Kiro streaming response to Claude format. +// It adds the required "event: " prefix for SSE compliance with Claude clients. +// Input format: "data: {\"type\":\"message_start\",...}" +// Output format: "event: message_start\ndata: {\"type\":\"message_start\",...}" func ConvertKiroResponseToClaude(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) []string { - return []string{string(rawResponse)} + raw := string(rawResponse) + + // Handle multiple data blocks (e.g., message_delta + message_stop) + lines := strings.Split(raw, "\n\n") + var results []string + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // Extract event type from JSON and add "event:" prefix + formatted := addEventPrefix(line) + if formatted != "" { + results = append(results, formatted) + } + } + + if len(results) == 0 { + return []string{raw} + } + + return results +} + +// addEventPrefix extracts the event type from the data line and adds the event: prefix. +// Input: "data: {\"type\":\"message_start\",...}" +// Output: "event: message_start\ndata: {\"type\":\"message_start\",...}" +func addEventPrefix(dataLine string) string { + if !strings.HasPrefix(dataLine, "data: ") { + return dataLine + } + + jsonPart := strings.TrimPrefix(dataLine, "data: ") + eventType := gjson.Get(jsonPart, "type").String() + + if eventType == "" { + return dataLine + } + + return "event: " + eventType + "\n" + dataLine } // ConvertKiroResponseToClaudeNonStream converts Kiro non-streaming response to Claude format. From 8d5f89ccfd02f4a3228a431f6d3cbbaf8a8887a1 Mon Sep 17 00:00:00 2001 From: Ravens Date: Thu, 11 Dec 2025 01:15:00 +0800 Subject: [PATCH 4/4] fix(kiro): fix translator format mismatch for OpenAI protocol Amp-Thread-ID: https://ampcode.com/threads/T-019b092b-f2de-72a1-b428-72511c0de628 Co-authored-by: Amp --- internal/runtime/executor/kiro_executor.go | 51 ++++++++--------- .../translator/kiro/claude/kiro_claude.go | 55 ++----------------- .../chat-completions/kiro_openai_response.go | 48 +++++++++++++++- 3 files changed, 77 insertions(+), 77 deletions(-) diff --git a/internal/runtime/executor/kiro_executor.go b/internal/runtime/executor/kiro_executor.go index b965c9ca..b69fd8be 100644 --- a/internal/runtime/executor/kiro_executor.go +++ b/internal/runtime/executor/kiro_executor.go @@ -1323,7 +1323,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Send message_start on first event if !messageStartSent { msgStart := e.buildClaudeMessageStartEvent(model, totalUsage.InputTokens) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, msgStart, &translatorParam) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgStart, &translatorParam) for _, chunk := range sseData { if chunk != "" { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} @@ -1372,7 +1372,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out contentBlockIndex++ isTextBlockOpen = true blockStart := e.buildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "") - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam) + 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")} @@ -1381,7 +1381,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out } claudeEvent := e.buildClaudeStreamEvent(contentDelta, contentBlockIndex) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam) for _, chunk := range sseData { if chunk != "" { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} @@ -1404,7 +1404,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Close text block if open before starting tool_use block if isTextBlockOpen && contentBlockIndex >= 0 { blockStop := e.buildClaudeContentBlockStopEvent(contentBlockIndex) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) + 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")} @@ -1418,7 +1418,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out toolName := getString(tu, "name") blockStart := e.buildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", toolUseID, toolName) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam) + 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")} @@ -1433,7 +1433,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Don't continue - still need to close the block } else { inputDelta := e.buildClaudeInputJsonDeltaEvent(string(inputJSON), contentBlockIndex) - sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam) + 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")} @@ -1444,7 +1444,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Close tool_use block (always close even if input marshal failed) blockStop := e.buildClaudeContentBlockStopEvent(contentBlockIndex) - sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) + 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")} @@ -1464,7 +1464,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Close text block if open if isTextBlockOpen && contentBlockIndex >= 0 { blockStop := e.buildClaudeContentBlockStopEvent(contentBlockIndex) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) + 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")} @@ -1476,7 +1476,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out contentBlockIndex++ blockStart := e.buildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", tu.ToolUseID, tu.Name) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam) + 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")} @@ -1489,7 +1489,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out log.Debugf("kiro: failed to marshal tool input in toolUseEvent: %v", err) } else { inputDelta := e.buildClaudeInputJsonDeltaEvent(string(inputJSON), contentBlockIndex) - sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam) + 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")} @@ -1499,7 +1499,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out } blockStop := e.buildClaudeContentBlockStopEvent(contentBlockIndex) - sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) + 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")} @@ -1530,7 +1530,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Close content block if open if isTextBlockOpen && contentBlockIndex >= 0 { blockStop := e.buildClaudeContentBlockStopEvent(contentBlockIndex) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) + 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")} @@ -1555,7 +1555,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Send message_delta and message_stop msgStop := e.buildClaudeMessageStopEvent(stopReason, totalUsage) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, msgStop, &translatorParam) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgStop, &translatorParam) for _, chunk := range sseData { if chunk != "" { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} @@ -1566,6 +1566,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out // Claude SSE event builders +// All builders return complete SSE format with "event:" line for Claude client compatibility. func (e *KiroExecutor) buildClaudeMessageStartEvent(model string, inputTokens int64) []byte { event := map[string]interface{}{ "type": "message_start", @@ -1581,7 +1582,7 @@ func (e *KiroExecutor) buildClaudeMessageStartEvent(model string, inputTokens in }, } result, _ := json.Marshal(event) - return []byte("data: " + string(result)) + return []byte("event: message_start\ndata: " + string(result)) } func (e *KiroExecutor) buildClaudeContentBlockStartEvent(index int, blockType, toolUseID, toolName string) []byte { @@ -1606,7 +1607,7 @@ func (e *KiroExecutor) buildClaudeContentBlockStartEvent(index int, blockType, t "content_block": contentBlock, } result, _ := json.Marshal(event) - return []byte("data: " + string(result)) + return []byte("event: content_block_start\ndata: " + string(result)) } func (e *KiroExecutor) buildClaudeStreamEvent(contentDelta string, index int) []byte { @@ -1619,7 +1620,7 @@ func (e *KiroExecutor) buildClaudeStreamEvent(contentDelta string, index int) [] }, } result, _ := json.Marshal(event) - return []byte("data: " + string(result)) + return []byte("event: content_block_delta\ndata: " + string(result)) } // buildClaudeInputJsonDeltaEvent creates an input_json_delta event for tool use streaming @@ -1633,7 +1634,7 @@ func (e *KiroExecutor) buildClaudeInputJsonDeltaEvent(partialJSON string, index }, } result, _ := json.Marshal(event) - return []byte("data: " + string(result)) + return []byte("event: content_block_delta\ndata: " + string(result)) } func (e *KiroExecutor) buildClaudeContentBlockStopEvent(index int) []byte { @@ -1642,7 +1643,7 @@ func (e *KiroExecutor) buildClaudeContentBlockStopEvent(index int) []byte { "index": index, } result, _ := json.Marshal(event) - return []byte("data: " + string(result)) + return []byte("event: content_block_stop\ndata: " + string(result)) } func (e *KiroExecutor) buildClaudeMessageStopEvent(stopReason string, usageInfo usage.Detail) []byte { @@ -1666,7 +1667,7 @@ func (e *KiroExecutor) buildClaudeMessageStopEvent(stopReason string, usageInfo } stopResult, _ := json.Marshal(stopEvent) - return []byte("data: " + string(deltaResult) + "\n\ndata: " + string(stopResult)) + return []byte("event: message_delta\ndata: " + string(deltaResult) + "\n\nevent: message_stop\ndata: " + string(stopResult)) } // buildClaudeFinalEvent constructs the final Claude-style event. @@ -1675,7 +1676,7 @@ func (e *KiroExecutor) buildClaudeFinalEvent() []byte { "type": "message_stop", } result, _ := json.Marshal(event) - return []byte("data: " + string(result)) + return []byte("event: message_stop\ndata: " + string(result)) } // CountTokens is not supported for the Kiro provider. @@ -1890,7 +1891,7 @@ func (e *KiroExecutor) streamEventStream(ctx context.Context, body io.Reader, c if !messageStartSent { msgStart := e.buildClaudeMessageStartEvent(model, totalUsage.InputTokens) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, msgStart, &translatorParam) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgStart, &translatorParam) for _, chunk := range sseData { if chunk != "" { c.Writer.Write([]byte(chunk + "\n\n")) @@ -1921,7 +1922,7 @@ func (e *KiroExecutor) streamEventStream(ctx context.Context, body io.Reader, c contentBlockIndex++ isBlockOpen = true blockStart := e.buildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "") - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam) for _, chunk := range sseData { if chunk != "" { c.Writer.Write([]byte(chunk + "\n\n")) @@ -1931,7 +1932,7 @@ func (e *KiroExecutor) streamEventStream(ctx context.Context, body io.Reader, c } claudeEvent := e.buildClaudeStreamEvent(contentDelta, contentBlockIndex) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam) for _, chunk := range sseData { if chunk != "" { c.Writer.Write([]byte(chunk + "\n\n")) @@ -1964,7 +1965,7 @@ func (e *KiroExecutor) streamEventStream(ctx context.Context, body io.Reader, c // Close content block if open if isBlockOpen && contentBlockIndex >= 0 { blockStop := e.buildClaudeContentBlockStopEvent(contentBlockIndex) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) for _, chunk := range sseData { if chunk != "" { c.Writer.Write([]byte(chunk + "\n\n")) @@ -1984,7 +1985,7 @@ func (e *KiroExecutor) streamEventStream(ctx context.Context, body io.Reader, c // Always use end_turn (no tool_use support) msgStop := e.buildClaudeMessageStopEvent("end_turn", totalUsage) - sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("claude"), targetFormat, model, originalReq, claudeBody, msgStop, &translatorParam) + sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgStop, &translatorParam) for _, chunk := range sseData { if chunk != "" { c.Writer.Write([]byte(chunk + "\n\n")) diff --git a/internal/translator/kiro/claude/kiro_claude.go b/internal/translator/kiro/claude/kiro_claude.go index 335873a7..554dbf21 100644 --- a/internal/translator/kiro/claude/kiro_claude.go +++ b/internal/translator/kiro/claude/kiro_claude.go @@ -1,14 +1,11 @@ // Package claude provides translation between Kiro and Claude formats. -// Since Kiro uses Claude-compatible format internally, translations are mostly pass-through. -// However, SSE events require proper "event: " prefix for Claude clients. +// Since Kiro executor generates Claude-compatible SSE format internally (with event: prefix), +// translations are pass-through. package claude import ( "bytes" "context" - "strings" - - "github.com/tidwall/gjson" ) // ConvertClaudeRequestToKiro converts Claude request to Kiro format. @@ -18,52 +15,10 @@ func ConvertClaudeRequestToKiro(modelName string, inputRawJSON []byte, stream bo } // ConvertKiroResponseToClaude converts Kiro streaming response to Claude format. -// It adds the required "event: " prefix for SSE compliance with Claude clients. -// Input format: "data: {\"type\":\"message_start\",...}" -// Output format: "event: message_start\ndata: {\"type\":\"message_start\",...}" +// Kiro executor already generates complete SSE format with "event:" prefix, +// so this is a simple pass-through. func ConvertKiroResponseToClaude(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) []string { - raw := string(rawResponse) - - // Handle multiple data blocks (e.g., message_delta + message_stop) - lines := strings.Split(raw, "\n\n") - var results []string - - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - // Extract event type from JSON and add "event:" prefix - formatted := addEventPrefix(line) - if formatted != "" { - results = append(results, formatted) - } - } - - if len(results) == 0 { - return []string{raw} - } - - return results -} - -// addEventPrefix extracts the event type from the data line and adds the event: prefix. -// Input: "data: {\"type\":\"message_start\",...}" -// Output: "event: message_start\ndata: {\"type\":\"message_start\",...}" -func addEventPrefix(dataLine string) string { - if !strings.HasPrefix(dataLine, "data: ") { - return dataLine - } - - jsonPart := strings.TrimPrefix(dataLine, "data: ") - eventType := gjson.Get(jsonPart, "type").String() - - if eventType == "" { - return dataLine - } - - return "event: " + eventType + "\n" + dataLine + return []string{string(rawResponse)} } // ConvertKiroResponseToClaudeNonStream converts Kiro non-streaming response to Claude format. diff --git a/internal/translator/kiro/openai/chat-completions/kiro_openai_response.go b/internal/translator/kiro/openai/chat-completions/kiro_openai_response.go index 6a0ad250..df75cc07 100644 --- a/internal/translator/kiro/openai/chat-completions/kiro_openai_response.go +++ b/internal/translator/kiro/openai/chat-completions/kiro_openai_response.go @@ -4,6 +4,7 @@ package chat_completions import ( "context" "encoding/json" + "strings" "time" "github.com/google/uuid" @@ -13,15 +14,58 @@ import ( // ConvertKiroResponseToOpenAI converts Kiro streaming response to OpenAI SSE format. // Handles Claude SSE events: content_block_start, content_block_delta, input_json_delta, // content_block_stop, message_delta, and message_stop. +// Input may be in SSE format: "event: xxx\ndata: {...}" or raw JSON. func ConvertKiroResponseToOpenAI(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) []string { - root := gjson.ParseBytes(rawResponse) + raw := string(rawResponse) + var results []string + + // Handle SSE format: extract JSON from "data: " lines + // Input format: "event: message_start\ndata: {...}" + lines := strings.Split(raw, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "data: ") { + jsonPart := strings.TrimPrefix(line, "data: ") + chunks := convertClaudeEventToOpenAI(jsonPart, model) + results = append(results, chunks...) + } else if strings.HasPrefix(line, "{") { + // Raw JSON (backward compatibility) + chunks := convertClaudeEventToOpenAI(line, model) + results = append(results, chunks...) + } + } + + return results +} + +// convertClaudeEventToOpenAI converts a single Claude JSON event to OpenAI format +func convertClaudeEventToOpenAI(jsonStr string, model string) []string { + root := gjson.Parse(jsonStr) var results []string eventType := root.Get("type").String() switch eventType { case "message_start": - // Initial message event - could emit initial chunk if needed + // Initial message event - emit initial chunk with role + response := map[string]interface{}{ + "id": "chatcmpl-" + uuid.New().String()[:24], + "object": "chat.completion.chunk", + "created": time.Now().Unix(), + "model": model, + "choices": []map[string]interface{}{ + { + "index": 0, + "delta": map[string]interface{}{ + "role": "assistant", + "content": "", + }, + "finish_reason": nil, + }, + }, + } + result, _ := json.Marshal(response) + results = append(results, string(result)) return results case "content_block_start":