feat(kiro): implement official reasoningContentEvent and improve metadat

This commit is contained in:
Ravens2121
2025-12-18 04:38:22 +08:00
parent 0155a01bb1
commit d687ee2777
3 changed files with 431 additions and 477 deletions
+403 -447
View File
@@ -1293,17 +1293,66 @@ func (e *KiroExecutor) parseEventStream(body io.Reader) (string, []kiroclaude.Ki
log.Debugf("kiro: parseEventStream found stopReason in messageStopEvent: %s", stopReason) log.Debugf("kiro: parseEventStream found stopReason in messageStopEvent: %s", stopReason)
} }
case "messageMetadataEvent": case "messageMetadataEvent", "metadataEvent":
// Handle message metadata events which may contain token counts // Handle message metadata events which contain token counts
if metadata, ok := event["messageMetadataEvent"].(map[string]interface{}); ok { // Official format: { tokenUsage: { outputTokens, totalTokens, uncachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens, contextUsagePercentage } }
var metadata map[string]interface{}
if m, ok := event["messageMetadataEvent"].(map[string]interface{}); ok {
metadata = m
} else if m, ok := event["metadataEvent"].(map[string]interface{}); ok {
metadata = m
} else {
metadata = event // event itself might be the metadata
}
// Check for nested tokenUsage object (official format)
if tokenUsage, ok := metadata["tokenUsage"].(map[string]interface{}); ok {
// outputTokens - precise output token count
if outputTokens, ok := tokenUsage["outputTokens"].(float64); ok {
usageInfo.OutputTokens = int64(outputTokens)
log.Infof("kiro: parseEventStream found precise outputTokens in tokenUsage: %d", usageInfo.OutputTokens)
}
// totalTokens - precise total token count
if totalTokens, ok := tokenUsage["totalTokens"].(float64); ok {
usageInfo.TotalTokens = int64(totalTokens)
log.Infof("kiro: parseEventStream found precise totalTokens in tokenUsage: %d", usageInfo.TotalTokens)
}
// uncachedInputTokens - input tokens not from cache
if uncachedInputTokens, ok := tokenUsage["uncachedInputTokens"].(float64); ok {
usageInfo.InputTokens = int64(uncachedInputTokens)
log.Infof("kiro: parseEventStream found uncachedInputTokens in tokenUsage: %d", usageInfo.InputTokens)
}
// cacheReadInputTokens - tokens read from cache
if cacheReadTokens, ok := tokenUsage["cacheReadInputTokens"].(float64); ok {
// Add to input tokens if we have uncached tokens, otherwise use as input
if usageInfo.InputTokens > 0 {
usageInfo.InputTokens += int64(cacheReadTokens)
} else {
usageInfo.InputTokens = int64(cacheReadTokens)
}
log.Debugf("kiro: parseEventStream found cacheReadInputTokens in tokenUsage: %d", int64(cacheReadTokens))
}
// contextUsagePercentage - can be used as fallback for input token estimation
if ctxPct, ok := tokenUsage["contextUsagePercentage"].(float64); ok {
upstreamContextPercentage = ctxPct
log.Debugf("kiro: parseEventStream found contextUsagePercentage in tokenUsage: %.2f%%", ctxPct)
}
}
// Fallback: check for direct fields in metadata (legacy format)
if usageInfo.InputTokens == 0 {
if inputTokens, ok := metadata["inputTokens"].(float64); ok { if inputTokens, ok := metadata["inputTokens"].(float64); ok {
usageInfo.InputTokens = int64(inputTokens) usageInfo.InputTokens = int64(inputTokens)
log.Debugf("kiro: parseEventStream found inputTokens in messageMetadataEvent: %d", usageInfo.InputTokens) log.Debugf("kiro: parseEventStream found inputTokens in messageMetadataEvent: %d", usageInfo.InputTokens)
} }
}
if usageInfo.OutputTokens == 0 {
if outputTokens, ok := metadata["outputTokens"].(float64); ok { if outputTokens, ok := metadata["outputTokens"].(float64); ok {
usageInfo.OutputTokens = int64(outputTokens) usageInfo.OutputTokens = int64(outputTokens)
log.Debugf("kiro: parseEventStream found outputTokens in messageMetadataEvent: %d", usageInfo.OutputTokens) log.Debugf("kiro: parseEventStream found outputTokens in messageMetadataEvent: %d", usageInfo.OutputTokens)
} }
}
if usageInfo.TotalTokens == 0 {
if totalTokens, ok := metadata["totalTokens"].(float64); ok { if totalTokens, ok := metadata["totalTokens"].(float64); ok {
usageInfo.TotalTokens = int64(totalTokens) usageInfo.TotalTokens = int64(totalTokens)
log.Debugf("kiro: parseEventStream found totalTokens in messageMetadataEvent: %d", usageInfo.TotalTokens) log.Debugf("kiro: parseEventStream found totalTokens in messageMetadataEvent: %d", usageInfo.TotalTokens)
@@ -1356,6 +1405,78 @@ func (e *KiroExecutor) parseEventStream(body io.Reader) (string, []kiroclaude.Ki
usageInfo.InputTokens, usageInfo.OutputTokens) usageInfo.InputTokens, usageInfo.OutputTokens)
} }
case "meteringEvent":
// Handle metering events from Kiro API (usage billing information)
// Official format: { unit: string, unitPlural: string, usage: number }
if metering, ok := event["meteringEvent"].(map[string]interface{}); ok {
unit := ""
if u, ok := metering["unit"].(string); ok {
unit = u
}
usageVal := 0.0
if u, ok := metering["usage"].(float64); ok {
usageVal = u
}
log.Infof("kiro: parseEventStream received meteringEvent: usage=%.2f %s", usageVal, unit)
// Store metering info for potential billing/statistics purposes
// Note: This is separate from token counts - it's AWS billing units
} else {
// Try direct fields
unit := ""
if u, ok := event["unit"].(string); ok {
unit = u
}
usageVal := 0.0
if u, ok := event["usage"].(float64); ok {
usageVal = u
}
if unit != "" || usageVal > 0 {
log.Infof("kiro: parseEventStream received meteringEvent (direct): usage=%.2f %s", usageVal, unit)
}
}
case "error", "exception", "internalServerException", "invalidStateEvent":
// Handle error events from Kiro API stream
errMsg := ""
errType := eventType
// Try to extract error message from various formats
if msg, ok := event["message"].(string); ok {
errMsg = msg
} else if errObj, ok := event[eventType].(map[string]interface{}); ok {
if msg, ok := errObj["message"].(string); ok {
errMsg = msg
}
if t, ok := errObj["type"].(string); ok {
errType = t
}
} else if errObj, ok := event["error"].(map[string]interface{}); ok {
if msg, ok := errObj["message"].(string); ok {
errMsg = msg
}
if t, ok := errObj["type"].(string); ok {
errType = t
}
}
// Check for specific error reasons
if reason, ok := event["reason"].(string); ok {
errMsg = fmt.Sprintf("%s (reason: %s)", errMsg, reason)
}
log.Errorf("kiro: parseEventStream received error event: type=%s, message=%s", errType, errMsg)
// For invalidStateEvent, we may want to continue processing other events
if eventType == "invalidStateEvent" {
log.Warnf("kiro: invalidStateEvent received, continuing stream processing")
continue
}
// For other errors, return the error
if errMsg != "" {
return "", nil, usageInfo, stopReason, fmt.Errorf("kiro API error (%s): %s", errType, errMsg)
}
default: default:
// Check for contextUsagePercentage in any event // Check for contextUsagePercentage in any event
if ctxPct, ok := event["contextUsagePercentage"].(float64); ok { if ctxPct, ok := event["contextUsagePercentage"].(float64); ok {
@@ -1693,30 +1814,14 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
// IMPORTANT: This must persist across all TranslateStream calls // IMPORTANT: This must persist across all TranslateStream calls
var translatorParam any var translatorParam any
// Thinking mode state tracking - based on amq2api implementation // Thinking mode state tracking - tag-based parsing for <thinking> tags in content
// Tracks whether we're inside a <thinking> block and handles partial tags inThinkBlock := false // Whether we're currently inside a <thinking> block
inThinkBlock := false isThinkingBlockOpen := false // Track if thinking content block SSE event is open
pendingStartTagChars := 0 // Number of chars that might be start of <thinking>
pendingEndTagChars := 0 // Number of chars that might be start of </thinking>
isThinkingBlockOpen := false // Track if thinking content block is open
thinkingBlockIndex := -1 // Index of the thinking content block thinkingBlockIndex := -1 // Index of the thinking content block
var accumulatedThinkingContent strings.Builder // Accumulate thinking content for signature generation var accumulatedThinkingContent strings.Builder // Accumulate thinking content for token counting
// Code block state tracking for heuristic thinking tag parsing // Buffer for handling partial tag matches at chunk boundaries
// When inside a markdown code block, <thinking> tags should NOT be parsed var pendingContent strings.Builder // Buffer content that might be part of a tag
// This prevents false positives when the model outputs code examples containing these tags
inCodeBlock := false
codeFenceType := "" // Track which fence type opened the block ("```" or "~~~")
// Inline code state tracking - when inside backticks, don't parse thinking tags
// This handles cases like `<thinking>` being discussed in text
inInlineCode := false
// Track if we've seen any non-whitespace content before a thinking tag
// Real thinking blocks from Kiro always start at the very beginning of the response
// If we see content before <thinking>, subsequent <thinking> tags are likely discussion text
hasSeenNonThinkingContent := false
thinkingBlockCompleted := false // Track if we've already completed a thinking block
// Pre-calculate input tokens from request if possible // Pre-calculate input tokens from request if possible
// Kiro uses Claude format, so try Claude format first, then OpenAI format, then fallback // Kiro uses Claude format, so try Claude format first, then OpenAI format, then fallback
@@ -1820,57 +1925,10 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
currentToolUse = nil currentToolUse = nil
} }
// Flush any pending tag characters at EOF // DISABLED: Tag-based pending character flushing
// These are partial tag prefixes that were held back waiting for more data // This code block was used for tag-based thinking detection which has been
// Since no more data is coming, output them as regular text // replaced by reasoningContentEvent handling. No pending tag chars to flush.
var pendingText string // Original code preserved in git history.
if pendingStartTagChars > 0 {
pendingText = kirocommon.ThinkingStartTag[:pendingStartTagChars]
log.Debugf("kiro: flushing pending start tag chars at EOF: %q", pendingText)
pendingStartTagChars = 0
}
if pendingEndTagChars > 0 {
pendingText += kirocommon.ThinkingEndTag[:pendingEndTagChars]
log.Debugf("kiro: flushing pending end tag chars at EOF: %q", pendingText)
pendingEndTagChars = 0
}
// Output pending text if any
if pendingText != "" {
// If we're in a thinking block, output as thinking content
if inThinkBlock && isThinkingBlockOpen {
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(pendingText, thinkingBlockIndex)
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
for _, chunk := range sseData {
if chunk != "" {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
}
}
// Accumulate thinking content for signature generation
accumulatedThinkingContent.WriteString(pendingText)
} else {
// Output as regular text
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(pendingText, contentBlockIndex)
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")}
}
}
}
}
break break
} }
@@ -1954,6 +2012,76 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
log.Debugf("kiro: streamToChannel found stopReason in messageStopEvent: %s", upstreamStopReason) log.Debugf("kiro: streamToChannel found stopReason in messageStopEvent: %s", upstreamStopReason)
} }
case "meteringEvent":
// Handle metering events from Kiro API (usage billing information)
// Official format: { unit: string, unitPlural: string, usage: number }
if metering, ok := event["meteringEvent"].(map[string]interface{}); ok {
unit := ""
if u, ok := metering["unit"].(string); ok {
unit = u
}
usageVal := 0.0
if u, ok := metering["usage"].(float64); ok {
usageVal = u
}
upstreamCreditUsage = usageVal
hasUpstreamUsage = true
log.Infof("kiro: streamToChannel received meteringEvent: usage=%.4f %s", usageVal, unit)
} else {
// Try direct fields (event is meteringEvent itself)
if unit, ok := event["unit"].(string); ok {
if usage, ok := event["usage"].(float64); ok {
upstreamCreditUsage = usage
hasUpstreamUsage = true
log.Infof("kiro: streamToChannel received meteringEvent (direct): usage=%.4f %s", usage, unit)
}
}
}
case "error", "exception", "internalServerException":
// Handle error events from Kiro API stream
errMsg := ""
errType := eventType
// Try to extract error message from various formats
if msg, ok := event["message"].(string); ok {
errMsg = msg
} else if errObj, ok := event[eventType].(map[string]interface{}); ok {
if msg, ok := errObj["message"].(string); ok {
errMsg = msg
}
if t, ok := errObj["type"].(string); ok {
errType = t
}
} else if errObj, ok := event["error"].(map[string]interface{}); ok {
if msg, ok := errObj["message"].(string); ok {
errMsg = msg
}
}
log.Errorf("kiro: streamToChannel received error event: type=%s, message=%s", errType, errMsg)
// Send error to the stream and exit
if errMsg != "" {
out <- cliproxyexecutor.StreamChunk{
Err: fmt.Errorf("kiro API error (%s): %s", errType, errMsg),
}
return
}
case "invalidStateEvent":
// Handle invalid state events - log and continue (non-fatal)
errMsg := ""
if msg, ok := event["message"].(string); ok {
errMsg = msg
} else if stateEvent, ok := event["invalidStateEvent"].(map[string]interface{}); ok {
if msg, ok := stateEvent["message"].(string); ok {
errMsg = msg
}
}
log.Warnf("kiro: streamToChannel received invalidStateEvent: %s, continuing", errMsg)
continue
default: default:
// Check for upstream usage events from Kiro API // Check for upstream usage events from Kiro API
// Format: {"unit":"credit","unitPlural":"credits","usage":1.458} // Format: {"unit":"credit","unitPlural":"credits","usage":1.458}
@@ -2110,266 +2238,22 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
lastUsageUpdateTime = time.Now() lastUsageUpdateTime = time.Now()
} }
// Process content with thinking tag detection - based on amq2api implementation // TAG-BASED THINKING PARSING: Parse <thinking> tags from content
// This handles <thinking> and </thinking> tags that may span across chunks // Combine pending content with new content for processing
remaining := contentDelta pendingContent.WriteString(contentDelta)
processContent := pendingContent.String()
// If we have pending start tag chars from previous chunk, prepend them pendingContent.Reset()
if pendingStartTagChars > 0 {
remaining = kirocommon.ThinkingStartTag[:pendingStartTagChars] + remaining
pendingStartTagChars = 0
}
// If we have pending end tag chars from previous chunk, prepend them
if pendingEndTagChars > 0 {
remaining = kirocommon.ThinkingEndTag[:pendingEndTagChars] + remaining
pendingEndTagChars = 0
}
for len(remaining) > 0 {
// CRITICAL FIX: Only parse <thinking> tags when thinking mode was enabled in the request.
// When thinking is NOT enabled, <thinking> tags in responses should be treated as
// regular text content, not as thinking blocks. This prevents normal text content
// from being incorrectly parsed as thinking when the model outputs <thinking> tags
// without the user requesting thinking mode.
if !thinkingEnabled {
// Thinking not enabled - emit all content as regular text without parsing tags
if remaining != "" {
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(remaining, contentBlockIndex)
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")}
}
}
}
break // Exit the for loop - all content processed as text
}
// HEURISTIC FIX: Track code block and inline code state to avoid parsing <thinking> tags
// inside code contexts. When the model outputs code examples containing these tags,
// they should be treated as text.
if !inThinkBlock {
// Check for inline code backticks first (higher priority than code fences)
// This handles cases like `<thinking>` being discussed in text
backtickIdx := strings.Index(remaining, kirocommon.InlineCodeMarker)
thinkingIdx := strings.Index(remaining, kirocommon.ThinkingStartTag)
// If backtick comes before thinking tag, handle inline code
if backtickIdx >= 0 && (thinkingIdx < 0 || backtickIdx < thinkingIdx) {
if inInlineCode {
// Closing backtick - emit content up to and including backtick, exit inline code
textToEmit := remaining[:backtickIdx+1]
if textToEmit != "" {
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(textToEmit, contentBlockIndex)
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")}
}
}
}
remaining = remaining[backtickIdx+1:]
inInlineCode = false
continue
} else {
// Opening backtick - emit content before backtick, enter inline code
textToEmit := remaining[:backtickIdx+1]
if textToEmit != "" {
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(textToEmit, contentBlockIndex)
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")}
}
}
}
remaining = remaining[backtickIdx+1:]
inInlineCode = true
continue
}
}
// If inside inline code, emit all content as text (don't parse thinking tags)
if inInlineCode {
if remaining != "" {
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(remaining, contentBlockIndex)
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")}
}
}
}
break // Exit loop - remaining content is inside inline code
}
// Check for code fence markers (``` or ~~~) to toggle code block state
fenceIdx := strings.Index(remaining, kirocommon.CodeFenceMarker)
altFenceIdx := strings.Index(remaining, kirocommon.AltCodeFenceMarker)
// Find the earliest fence marker
earliestFenceIdx := -1
earliestFenceType := ""
if fenceIdx >= 0 && (altFenceIdx < 0 || fenceIdx < altFenceIdx) {
earliestFenceIdx = fenceIdx
earliestFenceType = kirocommon.CodeFenceMarker
} else if altFenceIdx >= 0 {
earliestFenceIdx = altFenceIdx
earliestFenceType = kirocommon.AltCodeFenceMarker
}
if earliestFenceIdx >= 0 {
// Check if this fence comes before any thinking tag
thinkingIdx := strings.Index(remaining, kirocommon.ThinkingStartTag)
if inCodeBlock {
// Inside code block - check if this fence closes it
if earliestFenceType == codeFenceType {
// This fence closes the code block
// Emit content up to and including the fence as text
textToEmit := remaining[:earliestFenceIdx+len(earliestFenceType)]
if textToEmit != "" {
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(textToEmit, contentBlockIndex)
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")}
}
}
}
remaining = remaining[earliestFenceIdx+len(earliestFenceType):]
inCodeBlock = false
codeFenceType = ""
log.Debugf("kiro: exited code block")
continue
}
} else if thinkingIdx < 0 || earliestFenceIdx < thinkingIdx {
// Not in code block, and fence comes before thinking tag (or no thinking tag)
// Emit content up to and including the fence as text, then enter code block
textToEmit := remaining[:earliestFenceIdx+len(earliestFenceType)]
if textToEmit != "" {
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(textToEmit, contentBlockIndex)
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")}
}
}
}
remaining = remaining[earliestFenceIdx+len(earliestFenceType):]
inCodeBlock = true
codeFenceType = earliestFenceType
log.Debugf("kiro: entered code block with fence: %s", earliestFenceType)
continue
}
}
// If inside code block, emit all content as text (don't parse thinking tags)
if inCodeBlock {
if remaining != "" {
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(remaining, contentBlockIndex)
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")}
}
}
}
break // Exit loop - all remaining content is inside code block
}
}
// Process content looking for thinking tags
for len(processContent) > 0 {
if inThinkBlock { if inThinkBlock {
// Inside thinking block - look for </thinking> end tag // We're inside a thinking block, look for </thinking>
// CRITICAL FIX: Skip </thinking> tags that are not the real end tag endIdx := strings.Index(processContent, kirocommon.ThinkingEndTag)
// This prevents false positives when thinking content discusses these tags
// Pass current code block/inline code state for accurate detection
endIdx := findRealThinkingEndTag(remaining, inCodeBlock, inInlineCode)
if endIdx >= 0 { if endIdx >= 0 {
// Found end tag - emit any content before end tag, then close block // Found end tag - emit thinking content before the tag
thinkContent := remaining[:endIdx] thinkingText := processContent[:endIdx]
if thinkContent != "" { if thinkingText != "" {
// TRUE STREAMING: Emit thinking content immediately // Ensure thinking block is open
// Start thinking block if not open
if !isThinkingBlockOpen { if !isThinkingBlockOpen {
contentBlockIndex++ contentBlockIndex++
thinkingBlockIndex = contentBlockIndex thinkingBlockIndex = contentBlockIndex
@@ -2382,22 +2266,16 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
} }
} }
// Send thinking delta
// Send thinking delta immediately thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(thinkingText, thinkingBlockIndex)
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(thinkContent, thinkingBlockIndex)
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam) sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
for _, chunk := range sseData { for _, chunk := range sseData {
if chunk != "" { if chunk != "" {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
} }
} }
// Accumulate thinking content for signature generation accumulatedThinkingContent.WriteString(thinkingText)
accumulatedThinkingContent.WriteString(thinkContent)
} }
// Note: Partial tag handling is done via pendingEndTagChars
// When the next chunk arrives, the partial tag will be reconstructed
// Close thinking block // Close thinking block
if isThinkingBlockOpen { if isThinkingBlockOpen {
blockStop := kiroclaude.BuildClaudeThinkingBlockStopEvent(thinkingBlockIndex) blockStop := kiroclaude.BuildClaudeThinkingBlockStopEvent(thinkingBlockIndex)
@@ -2408,31 +2286,25 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
} }
isThinkingBlockOpen = false isThinkingBlockOpen = false
accumulatedThinkingContent.Reset() // Reset for potential next thinking block
} }
inThinkBlock = false inThinkBlock = false
thinkingBlockCompleted = true // Mark that we've completed a thinking block processContent = processContent[endIdx+len(kirocommon.ThinkingEndTag):]
remaining = remaining[endIdx+len(kirocommon.ThinkingEndTag):] log.Debugf("kiro: closed thinking block, remaining content: %d chars", len(processContent))
log.Debugf("kiro: exited thinking block, subsequent <thinking> tags will be treated as text")
} else { } else {
// No end tag found - TRUE STREAMING: emit content immediately // No end tag found - check for partial match at end
// Only save potential partial tag length for next iteration partialMatch := false
pendingEnd := kiroclaude.PendingTagSuffix(remaining, kirocommon.ThinkingEndTag) for i := 1; i < len(kirocommon.ThinkingEndTag) && i <= len(processContent); i++ {
if strings.HasSuffix(processContent, kirocommon.ThinkingEndTag[:i]) {
// Calculate content to emit immediately (excluding potential partial tag) // Possible partial tag at end, buffer it
var contentToEmit string pendingContent.WriteString(processContent[len(processContent)-i:])
if pendingEnd > 0 { processContent = processContent[:len(processContent)-i]
contentToEmit = remaining[:len(remaining)-pendingEnd] partialMatch = true
// Save partial tag length for next iteration (will be reconstructed from thinkingEndTag) break
pendingEndTagChars = pendingEnd
} else {
contentToEmit = remaining
} }
}
// TRUE STREAMING: Emit thinking content immediately if !partialMatch || len(processContent) > 0 {
if contentToEmit != "" { // Emit all as thinking content
// Start thinking block if not open if processContent != "" {
if !isThinkingBlockOpen { if !isThinkingBlockOpen {
contentBlockIndex++ contentBlockIndex++
thinkingBlockIndex = contentBlockIndex thinkingBlockIndex = contentBlockIndex
@@ -2445,47 +2317,37 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
} }
} }
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(processContent, thinkingBlockIndex)
// Send thinking delta immediately - TRUE STREAMING!
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(contentToEmit, thinkingBlockIndex)
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam) sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
for _, chunk := range sseData { for _, chunk := range sseData {
if chunk != "" { if chunk != "" {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")} out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
} }
} }
// Accumulate thinking content for signature generation accumulatedThinkingContent.WriteString(processContent)
accumulatedThinkingContent.WriteString(contentToEmit)
} }
}
remaining = "" processContent = ""
} }
} else { } else {
// Outside thinking block - look for <thinking> start tag // Not in thinking block, look for <thinking>
// CRITICAL FIX: Only parse <thinking> tags at the very beginning of the response startIdx := strings.Index(processContent, kirocommon.ThinkingStartTag)
// or if we haven't completed a thinking block yet.
// After a thinking block is completed, subsequent <thinking> tags are likely
// discussion text (e.g., "Kiro returns `<thinking>` tags") and should NOT be parsed.
startIdx := -1
if !thinkingBlockCompleted && !hasSeenNonThinkingContent {
startIdx = strings.Index(remaining, kirocommon.ThinkingStartTag)
// If there's non-whitespace content before the tag, it's not a real thinking block
if startIdx > 0 {
textBefore := remaining[:startIdx]
if strings.TrimSpace(textBefore) != "" {
// There's real content before the tag - this is discussion text, not thinking
hasSeenNonThinkingContent = true
startIdx = -1
log.Debugf("kiro: found <thinking> tag after non-whitespace content, treating as text")
}
}
}
if startIdx >= 0 { if startIdx >= 0 {
// Found start tag - emit text before it and switch to thinking mode // Found start tag - emit text content before the tag
textBefore := remaining[:startIdx] textBefore := processContent[:startIdx]
if textBefore != "" { if textBefore != "" {
// Only whitespace before thinking tag is allowed // Close thinking block if open
// Start text content block if needed if isThinkingBlockOpen {
blockStop := kiroclaude.BuildClaudeThinkingBlockStopEvent(thinkingBlockIndex)
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")}
}
}
isThinkingBlockOpen = false
}
// Ensure text block is open
if !isTextBlockOpen { if !isTextBlockOpen {
contentBlockIndex++ contentBlockIndex++
isTextBlockOpen = true isTextBlockOpen = true
@@ -2497,7 +2359,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
} }
} }
// Send text delta
claudeEvent := kiroclaude.BuildClaudeStreamEvent(textBefore, contentBlockIndex) claudeEvent := kiroclaude.BuildClaudeStreamEvent(textBefore, contentBlockIndex)
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam) sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam)
for _, chunk := range sseData { for _, chunk := range sseData {
@@ -2506,8 +2368,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
} }
} }
// Close text block before entering thinking
// Close text block before starting thinking block
if isTextBlockOpen { if isTextBlockOpen {
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex) blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam) sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
@@ -2518,26 +2379,24 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
isTextBlockOpen = false isTextBlockOpen = false
} }
inThinkBlock = true inThinkBlock = true
remaining = remaining[startIdx+len(kirocommon.ThinkingStartTag):] processContent = processContent[startIdx+len(kirocommon.ThinkingStartTag):]
log.Debugf("kiro: entered thinking block") log.Debugf("kiro: entered thinking block")
} else { } else {
// No start tag found - check for partial start tag at buffer end // No start tag found - check for partial match at end
// Only check for partial tags if we haven't completed a thinking block yet partialMatch := false
pendingStart := 0 for i := 1; i < len(kirocommon.ThinkingStartTag) && i <= len(processContent); i++ {
if !thinkingBlockCompleted && !hasSeenNonThinkingContent { if strings.HasSuffix(processContent, kirocommon.ThinkingStartTag[:i]) {
pendingStart = kiroclaude.PendingTagSuffix(remaining, kirocommon.ThinkingStartTag) // Possible partial tag at end, buffer it
pendingContent.WriteString(processContent[len(processContent)-i:])
processContent = processContent[:len(processContent)-i]
partialMatch = true
break
} }
if pendingStart > 0 {
// Emit text except potential partial tag
textToEmit := remaining[:len(remaining)-pendingStart]
if textToEmit != "" {
// Mark that we've seen non-thinking content
if strings.TrimSpace(textToEmit) != "" {
hasSeenNonThinkingContent = true
} }
// Start text content block if needed if !partialMatch || len(processContent) > 0 {
// Emit all as text content
if processContent != "" {
if !isTextBlockOpen { if !isTextBlockOpen {
contentBlockIndex++ contentBlockIndex++
isTextBlockOpen = true isTextBlockOpen = true
@@ -2549,8 +2408,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
} }
} }
claudeEvent := kiroclaude.BuildClaudeStreamEvent(processContent, contentBlockIndex)
claudeEvent := kiroclaude.BuildClaudeStreamEvent(textToEmit, contentBlockIndex)
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam) sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam)
for _, chunk := range sseData { for _, chunk := range sseData {
if chunk != "" { if chunk != "" {
@@ -2558,38 +2416,8 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
} }
} }
pendingStartTagChars = pendingStart
remaining = ""
} else {
// No partial tag - emit all as text
if remaining != "" {
// Mark that we've seen non-thinking content
if strings.TrimSpace(remaining) != "" {
hasSeenNonThinkingContent = true
}
// Start text content block if needed
if !isTextBlockOpen {
contentBlockIndex++
isTextBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
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")}
}
}
}
claudeEvent := kiroclaude.BuildClaudeStreamEvent(remaining, contentBlockIndex)
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")}
}
}
}
remaining = ""
} }
processContent = ""
} }
} }
} }
@@ -2658,6 +2486,80 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
} }
} }
case "reasoningContentEvent":
// Handle official reasoningContentEvent from Kiro API
// This replaces tag-based thinking detection with the proper event type
// Official format: { text: string, signature?: string, redactedContent?: base64 }
var thinkingText string
var signature string
if re, ok := event["reasoningContentEvent"].(map[string]interface{}); ok {
if text, ok := re["text"].(string); ok {
thinkingText = text
}
if sig, ok := re["signature"].(string); ok {
signature = sig
if len(sig) > 20 {
log.Debugf("kiro: reasoningContentEvent has signature: %s...", sig[:20])
} else {
log.Debugf("kiro: reasoningContentEvent has signature: %s", sig)
}
}
} else {
// Try direct fields
if text, ok := event["text"].(string); ok {
thinkingText = text
}
if sig, ok := event["signature"].(string); ok {
signature = sig
}
}
if thinkingText != "" {
// Close text block if open before starting thinking block
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
}
// Start thinking block if not already open
if !isThinkingBlockOpen {
contentBlockIndex++
thinkingBlockIndex = contentBlockIndex
isThinkingBlockOpen = true
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(thinkingBlockIndex, "thinking", "", "")
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")}
}
}
}
// Send thinking content
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(thinkingText, thinkingBlockIndex)
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
for _, chunk := range sseData {
if chunk != "" {
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
}
}
// Accumulate for token counting
accumulatedThinkingContent.WriteString(thinkingText)
log.Debugf("kiro: received reasoningContentEvent, text length: %d, has signature: %v", len(thinkingText), signature != "")
}
// Note: We don't close the thinking block here - it will be closed when we see
// the next assistantResponseEvent or at the end of the stream
_ = signature // Signature can be used for verification if needed
case "toolUseEvent": case "toolUseEvent":
// Handle dedicated tool use events with input buffering // Handle dedicated tool use events with input buffering
completedToolUses, newState := kiroclaude.ProcessToolUseEvent(event, currentToolUse, processedIDs) completedToolUses, newState := kiroclaude.ProcessToolUseEvent(event, currentToolUse, processedIDs)
@@ -2721,17 +2623,71 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
totalUsage.OutputTokens = int64(outputTokens) totalUsage.OutputTokens = int64(outputTokens)
} }
case "messageMetadataEvent": case "messageMetadataEvent", "metadataEvent":
// Handle message metadata events which may contain token counts // Handle message metadata events which contain token counts
if metadata, ok := event["messageMetadataEvent"].(map[string]interface{}); ok { // Official format: { tokenUsage: { outputTokens, totalTokens, uncachedInputTokens, cacheReadInputTokens, cacheWriteInputTokens, contextUsagePercentage } }
var metadata map[string]interface{}
if m, ok := event["messageMetadataEvent"].(map[string]interface{}); ok {
metadata = m
} else if m, ok := event["metadataEvent"].(map[string]interface{}); ok {
metadata = m
} else {
metadata = event // event itself might be the metadata
}
// Check for nested tokenUsage object (official format)
if tokenUsage, ok := metadata["tokenUsage"].(map[string]interface{}); ok {
// outputTokens - precise output token count
if outputTokens, ok := tokenUsage["outputTokens"].(float64); ok {
totalUsage.OutputTokens = int64(outputTokens)
hasUpstreamUsage = true
log.Infof("kiro: streamToChannel found precise outputTokens in tokenUsage: %d", totalUsage.OutputTokens)
}
// totalTokens - precise total token count
if totalTokens, ok := tokenUsage["totalTokens"].(float64); ok {
totalUsage.TotalTokens = int64(totalTokens)
log.Infof("kiro: streamToChannel found precise totalTokens in tokenUsage: %d", totalUsage.TotalTokens)
}
// uncachedInputTokens - input tokens not from cache
if uncachedInputTokens, ok := tokenUsage["uncachedInputTokens"].(float64); ok {
totalUsage.InputTokens = int64(uncachedInputTokens)
hasUpstreamUsage = true
log.Infof("kiro: streamToChannel found uncachedInputTokens in tokenUsage: %d", totalUsage.InputTokens)
}
// cacheReadInputTokens - tokens read from cache
if cacheReadTokens, ok := tokenUsage["cacheReadInputTokens"].(float64); ok {
// Add to input tokens if we have uncached tokens, otherwise use as input
if totalUsage.InputTokens > 0 {
totalUsage.InputTokens += int64(cacheReadTokens)
} else {
totalUsage.InputTokens = int64(cacheReadTokens)
}
hasUpstreamUsage = true
log.Debugf("kiro: streamToChannel found cacheReadInputTokens in tokenUsage: %d", int64(cacheReadTokens))
}
// contextUsagePercentage - can be used as fallback for input token estimation
if ctxPct, ok := tokenUsage["contextUsagePercentage"].(float64); ok {
upstreamContextPercentage = ctxPct
log.Debugf("kiro: streamToChannel found contextUsagePercentage in tokenUsage: %.2f%%", ctxPct)
}
}
// Fallback: check for direct fields in metadata (legacy format)
if totalUsage.InputTokens == 0 {
if inputTokens, ok := metadata["inputTokens"].(float64); ok { if inputTokens, ok := metadata["inputTokens"].(float64); ok {
totalUsage.InputTokens = int64(inputTokens) totalUsage.InputTokens = int64(inputTokens)
hasUpstreamUsage = true
log.Debugf("kiro: streamToChannel found inputTokens in messageMetadataEvent: %d", totalUsage.InputTokens) log.Debugf("kiro: streamToChannel found inputTokens in messageMetadataEvent: %d", totalUsage.InputTokens)
} }
}
if totalUsage.OutputTokens == 0 {
if outputTokens, ok := metadata["outputTokens"].(float64); ok { if outputTokens, ok := metadata["outputTokens"].(float64); ok {
totalUsage.OutputTokens = int64(outputTokens) totalUsage.OutputTokens = int64(outputTokens)
hasUpstreamUsage = true
log.Debugf("kiro: streamToChannel found outputTokens in messageMetadataEvent: %d", totalUsage.OutputTokens) log.Debugf("kiro: streamToChannel found outputTokens in messageMetadataEvent: %d", totalUsage.OutputTokens)
} }
}
if totalUsage.TotalTokens == 0 {
if totalTokens, ok := metadata["totalTokens"].(float64); ok { if totalTokens, ok := metadata["totalTokens"].(float64); ok {
totalUsage.TotalTokens = int64(totalTokens) totalUsage.TotalTokens = int64(totalTokens)
log.Debugf("kiro: streamToChannel found totalTokens in messageMetadataEvent: %d", totalUsage.TotalTokens) log.Debugf("kiro: streamToChannel found totalTokens in messageMetadataEvent: %d", totalUsage.TotalTokens)
@@ -222,20 +222,19 @@ func BuildKiroPayload(claudeBody []byte, modelID, profileArn, origin string, isA
kiroTools := convertClaudeToolsToKiro(tools) kiroTools := convertClaudeToolsToKiro(tools)
// Thinking mode implementation: // Thinking mode implementation:
// Kiro API doesn't accept max_tokens for thinking. Instead, thinking mode is enabled // Kiro API supports official thinking/reasoning mode via <thinking_mode> tag.
// by injecting <thinking_mode> and <max_thinking_length> tags into the system prompt. // When set to "enabled", Kiro returns reasoning content as official reasoningContentEvent
// We use a fixed max_thinking_length value since Kiro handles the actual budget internally. // rather than inline <thinking> tags in assistantResponseEvent.
// We use a high max_thinking_length to allow extensive reasoning.
if thinkingEnabled { if thinkingEnabled {
thinkingHint := `<thinking_mode>interleaved</thinking_mode> thinkingHint := `<thinking_mode>enabled</thinking_mode>
<max_thinking_length>200000</max_thinking_length> <max_thinking_length>200000</max_thinking_length>`
IMPORTANT: You MUST use <thinking>...</thinking> tags to show your reasoning process before providing your final response. Think step by step inside the thinking tags.`
if systemPrompt != "" { if systemPrompt != "" {
systemPrompt = thinkingHint + "\n\n" + systemPrompt systemPrompt = thinkingHint + "\n\n" + systemPrompt
} else { } else {
systemPrompt = thinkingHint systemPrompt = thinkingHint
} }
log.Infof("kiro: injected thinking prompt, has_tools: %v", len(kiroTools) > 0) log.Infof("kiro: injected thinking prompt (official mode), has_tools: %v", len(kiroTools) > 0)
} }
// Process messages and build history // Process messages and build history
@@ -231,20 +231,19 @@ func BuildKiroPayloadFromOpenAI(openaiBody []byte, modelID, profileArn, origin s
kiroTools := convertOpenAIToolsToKiro(tools) kiroTools := convertOpenAIToolsToKiro(tools)
// Thinking mode implementation: // Thinking mode implementation:
// Kiro API doesn't accept max_tokens for thinking. Instead, thinking mode is enabled // Kiro API supports official thinking/reasoning mode via <thinking_mode> tag.
// by injecting <thinking_mode> and <max_thinking_length> tags into the system prompt. // When set to "enabled", Kiro returns reasoning content as official reasoningContentEvent
// We use a fixed max_thinking_length value since Kiro handles the actual budget internally. // rather than inline <thinking> tags in assistantResponseEvent.
// We use a high max_thinking_length to allow extensive reasoning.
if thinkingEnabled { if thinkingEnabled {
thinkingHint := `<thinking_mode>interleaved</thinking_mode> thinkingHint := `<thinking_mode>enabled</thinking_mode>
<max_thinking_length>200000</max_thinking_length> <max_thinking_length>200000</max_thinking_length>`
IMPORTANT: You MUST use <thinking>...</thinking> tags to show your reasoning process before providing your final response. Think step by step inside the thinking tags.`
if systemPrompt != "" { if systemPrompt != "" {
systemPrompt = thinkingHint + "\n\n" + systemPrompt systemPrompt = thinkingHint + "\n\n" + systemPrompt
} else { } else {
systemPrompt = thinkingHint systemPrompt = thinkingHint
} }
log.Debugf("kiro-openai: injected thinking prompt") log.Debugf("kiro-openai: injected thinking prompt (official mode)")
} }
// Process messages and build history // Process messages and build history