Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ The setting takes effect immediately without restarting.
|----------|-------------|---------|
| `CONFIG_PATH` | Config file path | `data/config.json` |
| `ADMIN_PASSWORD` | Admin panel password (overrides config) | - |
| `KIRO_DIAGNOSTICS` | Enables stream, payload, and reasoning diagnostics | `false` |
| `KIRO_DIAG_STREAM` | Logs aggregate stream metrics | `false` |
| `KIRO_DIAG_PAYLOAD` | Logs payload size and truncation metrics | `false` |
| `KIRO_DIAG_REASONING` | Logs reasoning capabilities and forwarded fields | `false` |
| `KIRO_DIAG_CHUNKS` | Logs raw and normalized stream chunks; may contain sensitive content | `false` |

Diagnostic logging is disabled by default.

Accepted enabled values: `1`, `true`, `yes`, `on`, `enabled`.

`KIRO_DIAGNOSTICS` does not enable `KIRO_DIAG_CHUNKS`.

## Contributing

Expand Down
60 changes: 60 additions & 0 deletions diagnostics/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package diagnostics

import (
"os"
"strings"
)

// Diagnostic flags are read once at startup.
// Raw chunk logs require a separate opt-in.
var (
masterEnabled = envBool("KIRO_DIAGNOSTICS")

streamEnabled = masterEnabled ||
envBool("KIRO_DIAG_STREAM")

payloadEnabled = masterEnabled ||
envBool("KIRO_DIAG_PAYLOAD")

reasoningEnabled = masterEnabled ||
envBool("KIRO_DIAG_REASONING")

chunksEnabled = envBool("KIRO_DIAG_CHUNKS")
)

func envBool(name string) bool {
value, exists := os.LookupEnv(name)
if !exists {
return false
}

switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "yes", "on", "enabled":
return true
default:
return false
}
}

func Stream() bool {
return streamEnabled
}

func Payload() bool {
return payloadEnabled
}

func Reasoning() bool {
return reasoningEnabled
}

func Chunks() bool {
return chunksEnabled
}

func Any() bool {
return streamEnabled ||
payloadEnabled ||
reasoningEnabled ||
chunksEnabled
}
186 changes: 177 additions & 9 deletions proxy/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"kiro-go/auth"
"kiro-go/config"
"kiro-go/diagnostics"
"kiro-go/logger"
"kiro-go/pool"
"net/http"
Expand Down Expand Up @@ -883,10 +884,61 @@ func mergeModelInfo(base ModelInfo, extra ModelInfo) ModelInfo {
if base.TokenLimits == nil {
base.TokenLimits = extra.TokenLimits
}
if len(base.AdditionalModelRequestFieldsSchema) == 0 &&
len(extra.AdditionalModelRequestFieldsSchema) > 0 {
base.AdditionalModelRequestFieldsSchema =
append(
json.RawMessage(nil),
extra.AdditionalModelRequestFieldsSchema...,
)
}
base.InputTypes = mergeStringLists(base.InputTypes, extra.InputTypes)
return base
}

// Read reasoning capabilities from the cached model schema.
func (h *Handler) reasoningCapabilityForModel(modelID string) ModelReasoningCapability {
normalizedID := strings.ToLower(
strings.TrimSpace(MapModel(modelID)),
)

h.modelsCacheMu.RLock()
models := append([]ModelInfo(nil), h.cachedModels...)
h.modelsCacheMu.RUnlock()

if len(models) == 0 {
h.refreshModelsCache()

h.modelsCacheMu.RLock()
models = append([]ModelInfo(nil), h.cachedModels...)
h.modelsCacheMu.RUnlock()
}

for _, model := range models {
currentID := strings.ToLower(
strings.TrimSpace(model.ModelId),
)
if currentID != normalizedID {
continue
}
capability := ParseModelReasoningCapability(model)

if diagnostics.Reasoning() && capability.SchemaParseError != "" {
logger.Warnf(
"[KiroReasoning] model=%s schema parse failed: %s",
modelID,
capability.SchemaParseError,
)
}

return capability
}

return ModelReasoningCapability{
ModelID: modelID,
}
}

func mergeStringLists(base []string, extra []string) []string {
if len(extra) == 0 {
return base
Expand Down Expand Up @@ -936,9 +988,22 @@ func (h *Handler) handleCountTokens(w http.ResponseWriter, r *http.Request) {
}

thinkingCfg := config.GetThinkingConfig()
actualModel, thinking := resolveClaudeThinkingMode(req.Model, req.Thinking, thinkingCfg.Suffix)
actualModel, thinkingRequested := resolveClaudeThinkingMode(req.Model, req.Thinking, thinkingCfg.Suffix)
req.Model = actualModel
effectiveReq := cloneClaudeRequestForThinking(&req, thinking)

capability := h.reasoningCapabilityForModel(actualModel)

additionalFields, nativeRequested, buildErr := BuildClaudeAdditionalModelRequestFields(&req, capability)
if buildErr != nil {
h.sendClaudeError(w, 400, "invalid_request_error", buildErr.Error())
return
}

thinkingRequested = thinkingRequested || nativeRequested

useLegacyThinkingPrompt := thinkingRequested && len(additionalFields) == 0

effectiveReq := cloneClaudeRequestForThinking(&req, useLegacyThinkingPrompt)

estimatedTokens := estimateClaudeRequestInputTokens(effectiveReq)
if estimatedTokens < 1 {
Expand All @@ -954,6 +1019,36 @@ func (h *Handler) handleClaudeMessages(w http.ResponseWriter, r *http.Request) {
h.handleClaudeMessagesInternal(w, r)
}

func claudeThinkingType(req *ClaudeRequest) string {
if req == nil || req.Thinking == nil {
return ""
}

return strings.TrimSpace(req.Thinking.Type)
}

func claudeThinkingBudget(req *ClaudeRequest) int {
if req == nil || req.Thinking == nil {
return 0
}

return req.Thinking.BudgetTokens
}

func claudeReasoningEffort(req *ClaudeRequest) string {
if req == nil {
return ""
}

if req.OutputConfig != nil {
if effort := strings.TrimSpace(req.OutputConfig.Effort); effort != "" {
return effort
}
}

return strings.TrimSpace(req.ReasoningEffort)
}

func (h *Handler) handleClaudeMessagesInternal(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method Not Allowed", 405)
Expand All @@ -977,17 +1072,59 @@ func (h *Handler) handleClaudeMessagesInternal(w http.ResponseWriter, r *http.Re
return
}

// 解析模型和 thinking 模式
thinkingCfg := config.GetThinkingConfig()
actualModel, thinking := resolveClaudeThinkingMode(req.Model, req.Thinking, thinkingCfg.Suffix)
requestedModel := req.Model
actualModel, legacyOrClientThinking := resolveClaudeThinkingMode(req.Model, req.Thinking, thinkingCfg.Suffix)

req.Model = actualModel
effectiveReq := cloneClaudeRequestForThinking(&req, thinking)

capability := h.reasoningCapabilityForModel(actualModel)

// Build only reasoning fields supported by this model.
additionalFields, nativeRequested, buildErr := BuildClaudeAdditionalModelRequestFields(&req, capability)
if buildErr != nil {
h.sendClaudeError(w, 400, "invalid_request_error", buildErr.Error())
return
}

thinking := legacyOrClientThinking || nativeRequested

if diagnostics.Reasoning() {
logger.Infof(
"[ClaudeThinking] requested=%t type=%q budgetTokens=%d effort=%q requestedModel=%q actualModel=%q",
thinking,
claudeThinkingType(&req),
claudeThinkingBudget(&req),
claudeReasoningEffort(&req),
requestedModel,
actualModel,
)
logger.Infof(
"[KiroReasoning] model=%s requested=%t schemaPath=%s supportsThinking=%t thinkingTypes=%v thinkingDisplays=%v supportedEfforts=%v fields=%s",
actualModel,
nativeRequested,
capability.EffortPath,
capability.SupportsThinking,
capability.ThinkingTypes,
capability.ThinkingDisplays,
capability.Efforts,
reasoningFieldsJSON(additionalFields),
)
}

useLegacyThinkingPrompt := thinking && len(additionalFields) == 0

effectiveReq := cloneClaudeRequestForThinking(&req, useLegacyThinkingPrompt)

thinkingResponseOpts := resolveClaudeThinkingResponseOptions(req.Thinking, thinkingCfg.ClaudeFormat)

estimatedInputTokens := estimateClaudeRequestInputTokens(effectiveReq)

cacheProfile := h.promptCache.BuildClaudeProfile(effectiveReq, estimatedInputTokens)

// 转换请求
kiroPayload := ClaudeToKiro(&req, thinking)
kiroPayload := ClaudeToKiro(&req, useLegacyThinkingPrompt)

kiroPayload.AdditionalModelRequestFields = additionalFields

// Stream or non-stream
apiKeyID := apiKeyIDFromContext(r.Context())
Expand Down Expand Up @@ -1472,6 +1609,20 @@ func (h *Handler) handleClaudeStream(w http.ResponseWriter, payload *KiroPayload
stopReason = "tool_use"
}

if diagnostics.Stream() {
logger.Infof(
"[ClaudeStream] completed model=%s account=%s stopReason=%s toolUses=%d outputChars=%d inputTokens=%d outputTokens=%d durationMs=%d",
model,
account.Email,
stopReason,
len(toolUses),
len([]rune(outputContent)),
inputTokens,
outputTokens,
time.Since(reqStart).Milliseconds(),
)
}

ensureMessageStart()
h.sendSSE(w, flusher, "message_delta", map[string]interface{}{
"type": "message_delta",
Expand Down Expand Up @@ -1857,11 +2008,28 @@ func (h *Handler) handleOpenAIChat(w http.ResponseWriter, r *http.Request) {

// 解析模型和 thinking 模式
thinkingCfg := config.GetThinkingConfig()
actualModel, thinking := ParseModelAndThinking(req.Model, thinkingCfg.Suffix)

actualModel, suffixThinking := ParseModelAndThinking(req.Model, thinkingCfg.Suffix)

req.Model = actualModel

capability := h.reasoningCapabilityForModel(actualModel)

additionalFields, nativeRequested, buildErr := BuildOpenAIAdditionalModelRequestFields(&req, capability)
if buildErr != nil {
h.sendOpenAIError(w, 400, "invalid_request_error", buildErr.Error())
return
}

thinking := suffixThinking || nativeRequested

useLegacyThinkingPrompt := thinking && len(additionalFields) == 0

estimatedInputTokens := estimateOpenAIRequestInputTokens(&req)

kiroPayload := OpenAIToKiro(&req, thinking)
kiroPayload := OpenAIToKiro(&req, useLegacyThinkingPrompt)

kiroPayload.AdditionalModelRequestFields = additionalFields

apiKeyID := apiKeyIDFromContext(r.Context())
// forwarded marks a request that already passed through one Kiro-Go pool, so a
Expand Down
Loading
Loading