package proxy import ( "encoding/json" "math" ) func estimateApproxTokens(text string) int { if text == "" { return 0 } runes := []rune(text) length := len(runes) if length == 0 { return 0 } if length < 5 { return max(1, int(math.Ceil(float64(length)/3.0))) } var regularAscii, digits, symbols, nonASCII int for _, r := range runes { switch { case r >= 0x80: nonASCII++ case r >= '0' && r <= '9': digits++ case (r >= '!' && r <= '/') || (r >= ':' && r <= '@') || (r >= '[' && r <= '`') || (r >= '{' && r <= '~'): symbols++ default: regularAscii++ } } estimated := int(math.Ceil( float64(regularAscii)/4.5 + float64(digits)/2.0 + float64(symbols)/1.5 + float64(nonASCII)/1.5, )) if estimated < 1 { return 1 } return estimated } func estimateClaudeRequestInputTokens(req *ClaudeRequest) int { if req == nil { return 0 } total := estimateClaudeValueTokens(req.System) for _, msg := range req.Messages { total += estimateClaudeValueTokens(msg.Content) } for _, tool := range req.Tools { total += estimateApproxTokens(tool.Name) total += estimateApproxTokens(tool.Description) total += estimateJSONTokens(tool.InputSchema) } return total } func estimateClaudeOutputTokens(content, thinkingContent string, toolUses []KiroToolUse) int { total := estimateApproxTokens(content) total += estimateApproxTokens(thinkingContent) for _, tu := range toolUses { total += estimateApproxTokens(tu.Name) total += estimateJSONTokens(tu.Input) } return total } func estimateClaudeValueTokens(v interface{}) int { switch value := v.(type) { case nil: return 0 case string: return estimateApproxTokens(value) case []interface{}: total := 0 for _, part := range value { total += estimateClaudeValueTokens(part) } return total case map[string]interface{}: typeName, _ := value["type"].(string) switch typeName { case "text": if text, ok := value["text"].(string); ok { return estimateApproxTokens(text) } case "thinking": if thinking, ok := value["thinking"].(string); ok { return estimateApproxTokens(thinking) } case "tool_use": total := 0 if name, ok := value["name"].(string); ok { total += estimateApproxTokens(name) } if input, ok := value["input"]; ok { total += estimateJSONTokens(input) } if total > 0 { return total } case "tool_result": if content, ok := value["content"]; ok { return estimateClaudeValueTokens(content) } } total := 0 if text, ok := value["text"].(string); ok { total += estimateApproxTokens(text) } if thinking, ok := value["thinking"].(string); ok { total += estimateApproxTokens(thinking) } if content, ok := value["content"]; ok { total += estimateClaudeValueTokens(content) } if total > 0 { return total } return estimateJSONTokens(value) default: return estimateJSONTokens(value) } } func estimateJSONTokens(v interface{}) int { if v == nil { return 0 } b, err := json.Marshal(v) if err != nil { return 0 } return estimateApproxTokens(string(b)) } func estimateOpenAIRequestInputTokens(req *OpenAIRequest) int { if req == nil { return 0 } total := 0 for _, msg := range req.Messages { total += estimateOpenAIContentTokens(msg.Content) total += estimateApproxTokens(msg.ToolCallID) for _, tc := range msg.ToolCalls { total += estimateApproxTokens(tc.Function.Name) total += estimateApproxTokens(tc.Function.Arguments) } } for _, tool := range req.Tools { total += estimateApproxTokens(tool.Function.Name) total += estimateApproxTokens(tool.Function.Description) total += estimateJSONTokens(tool.Function.Parameters) } return total } func estimateOpenAIContentTokens(content interface{}) int { switch value := content.(type) { case nil: return 0 case string: return estimateApproxTokens(value) default: text := extractOpenAIMessageText(value) if text != "" { return estimateApproxTokens(text) } return estimateJSONTokens(value) } } func estimateOpenAIOutputTokens(content, reasoningContent string, toolUses []KiroToolUse) int { return estimateClaudeOutputTokens(content, reasoningContent, toolUses) }