package helps import ( "bytes" "context" "encoding/json" "fmt" "strings" "sync" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" "github.com/tiktoken-go/tokenizer" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" ) var ( claudeInputTokenizerOnce sync.Once claudeInputTokenizerCodec tokenizer.Codec claudeInputTokenizerErr error ) // ClaudeInputTokenState tracks the one-time input token update for a translated Claude stream. type ClaudeInputTokenState struct { upstreamFormat sdktranslator.Format responseFormat sdktranslator.Format originalRequest []byte codec tokenizer.Codec handled bool } // NewClaudeInputTokenState creates request-scoped state for translated Claude input token usage. func NewClaudeInputTokenState(sourceFormat, upstreamFormat, responseFormat sdktranslator.Format, originalRequest []byte) *ClaudeInputTokenState { enabled := sourceFormat == sdktranslator.FormatClaude && upstreamFormat != sdktranslator.FormatClaude && responseFormat == sdktranslator.FormatClaude return &ClaudeInputTokenState{ upstreamFormat: upstreamFormat, responseFormat: responseFormat, originalRequest: originalRequest, handled: !enabled, } } // TranslateStreamWithClaudeInputTokens translates a stream chunk and estimates Claude message_start input usage once. func TranslateStreamWithClaudeInputTokens( ctx context.Context, upstreamFormat, responseFormat sdktranslator.Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any, state *ClaudeInputTokenState, ) [][]byte { chunks := sdktranslator.TranslateStream( ctx, upstreamFormat, responseFormat, model, originalRequestRawJSON, requestRawJSON, rawJSON, param, ) if responseFormat == sdktranslator.FormatOpenAIResponse { for i, chunk := range chunks { chunks[i] = EnsureResponsesUsageDetails(chunk) } } if state == nil { return chunks } return state.apply(ctx, chunks) } func claudeInputTokenizer() (tokenizer.Codec, error) { claudeInputTokenizerOnce.Do(func() { claudeInputTokenizerCodec, claudeInputTokenizerErr = tokenizer.Get(tokenizer.O200kBase) }) return claudeInputTokenizerCodec, claudeInputTokenizerErr } // CountClaudeInputTokens estimates tokens for a Claude request with the O200kBase tokenizer. func CountClaudeInputTokens(payload []byte) (int64, error) { enc, err := claudeInputTokenizer() if err != nil { return 0, fmt.Errorf("initialize O200kBase tokenizer: %w", err) } count, err := countClaudeInputTokens(enc, payload) if err != nil { return 0, fmt.Errorf("count Claude input tokens: %w", err) } return count, nil } func countClaudeInputTokens(enc tokenizer.Codec, payload []byte) (int64, error) { if enc == nil { return 0, fmt.Errorf("encoder is nil") } segments, err := collectClaudeInputTokenSegments(payload) if err != nil { return 0, err } if len(segments) == 0 { return 0, nil } count, err := enc.Count(strings.Join(segments, "\n")) if err != nil { return 0, err } return int64(count), nil } func collectClaudeInputTokenSegments(payload []byte) ([]string, error) { if len(bytes.TrimSpace(payload)) == 0 { return nil, nil } if !gjson.ValidBytes(payload) { return nil, fmt.Errorf("invalid Claude request JSON") } root := gjson.ParseBytes(payload) segments := make([]string, 0, 32) collectClaudeSystemTokenSegments(root.Get("system"), &segments) collectClaudeMessageTokenSegments(root.Get("messages"), &segments) collectClaudeToolTokenSegments(root.Get("tools"), &segments) collectClaudeToolChoiceTokenSegments(root.Get("tool_choice"), &segments) return segments, nil } func collectClaudeSystemTokenSegments(system gjson.Result, segments *[]string) { if system.Type == gjson.String { appendClaudeTokenString(segments, system.String()) return } if !system.IsArray() { return } system.ForEach(func(_, part gjson.Result) bool { if part.Type == gjson.String { appendClaudeTokenString(segments, part.String()) } else if part.Get("type").String() == "text" { appendClaudeTokenString(segments, part.Get("text").String()) } return true }) } func collectClaudeMessageTokenSegments(messages gjson.Result, segments *[]string) { if !messages.IsArray() { return } messages.ForEach(func(_, message gjson.Result) bool { appendClaudeTokenString(segments, message.Get("role").String()) collectClaudeContentTokenSegments(message.Get("content"), segments) return true }) } func collectClaudeContentTokenSegments(content gjson.Result, segments *[]string) { if !content.Exists() { return } if content.Type == gjson.String { appendClaudeTokenString(segments, content.String()) return } if content.IsArray() { content.ForEach(func(_, part gjson.Result) bool { collectClaudeContentTokenSegments(part, segments) return true }) return } if !content.IsObject() { return } switch content.Get("type").String() { case "text": appendClaudeTokenString(segments, content.Get("text").String()) case "thinking": appendClaudeTokenString(segments, content.Get("thinking").String()) case "document": collectClaudeDocumentTokenSegments(content, segments) case "tool_use", "server_tool_use", "mcp_tool_use": appendClaudeTokenString(segments, content.Get("id").String()) appendClaudeTokenString(segments, content.Get("name").String()) appendClaudeTokenJSON(segments, content.Get("input")) case "tool_result", "mcp_tool_result", "web_search_tool_result", "web_fetch_tool_result", "code_execution_tool_result", "bash_code_execution_tool_result", "text_editor_code_execution_tool_result": appendClaudeTokenString(segments, content.Get("tool_use_id").String()) appendClaudeTokenString(segments, content.Get("tool_call_id").String()) collectClaudeContentTokenSegments(content.Get("content"), segments) case "web_search_result", "search_result": if source := content.Get("source"); source.Type == gjson.String { appendClaudeTokenString(segments, source.String()) } appendClaudeTokenString(segments, content.Get("title").String()) appendClaudeTokenString(segments, content.Get("url").String()) appendClaudeTokenString(segments, content.Get("page_age").String()) collectClaudeContentTokenSegments(content.Get("content"), segments) case "web_fetch_result": appendClaudeTokenString(segments, content.Get("url").String()) appendClaudeTokenString(segments, content.Get("retrieved_at").String()) collectClaudeContentTokenSegments(content.Get("content"), segments) case "code_execution_result", "bash_code_execution_result", "text_editor_code_execution_result": appendClaudeTokenString(segments, content.Get("stdout").String()) appendClaudeTokenString(segments, content.Get("stderr").String()) appendClaudeTokenString(segments, content.Get("return_code").String()) collectClaudeContentTokenSegments(content.Get("content"), segments) collectClaudeContentTokenSegments(content.Get("output"), segments) case "tool_reference": appendClaudeTokenString(segments, content.Get("tool_name").String()) case "image", "input_audio", "audio", "video", "redacted_thinking": return case "": appendClaudeTokenJSON(segments, content) default: appendClaudeTokenString(segments, content.Get("text").String()) } } func collectClaudeDocumentTokenSegments(document gjson.Result, segments *[]string) { source := document.Get("source") if source.Get("type").String() != "text" { return } appendClaudeTokenString(segments, document.Get("title").String()) appendClaudeTokenString(segments, document.Get("context").String()) appendClaudeTokenString(segments, source.Get("data").String()) appendClaudeTokenString(segments, source.Get("content").String()) } func collectClaudeToolTokenSegments(tools gjson.Result, segments *[]string) { if !tools.IsArray() { return } tools.ForEach(func(_, tool gjson.Result) bool { appendClaudeTokenString(segments, tool.Get("type").String()) appendClaudeTokenString(segments, tool.Get("name").String()) appendClaudeTokenString(segments, tool.Get("description").String()) appendClaudeTokenJSON(segments, tool.Get("input_schema")) return true }) } func collectClaudeToolChoiceTokenSegments(toolChoice gjson.Result, segments *[]string) { if !toolChoice.Exists() { return } if toolChoice.Type == gjson.String { appendClaudeTokenString(segments, toolChoice.String()) return } appendClaudeTokenString(segments, toolChoice.Get("type").String()) appendClaudeTokenString(segments, toolChoice.Get("name").String()) } func appendClaudeTokenString(segments *[]string, value string) { if segments == nil { return } if trimmed := strings.TrimSpace(value); trimmed != "" { *segments = append(*segments, trimmed) } } func appendClaudeTokenJSON(segments *[]string, value gjson.Result) { if !value.Exists() { return } if value.Type == gjson.String { appendClaudeTokenString(segments, value.String()) return } raw := strings.TrimSpace(value.Raw) if raw == "" { return } var compact bytes.Buffer if err := json.Compact(&compact, []byte(raw)); err == nil { appendClaudeTokenString(segments, compact.String()) return } appendClaudeTokenString(segments, raw) } func (state *ClaudeInputTokenState) apply(ctx context.Context, chunks [][]byte) [][]byte { if state == nil || state.handled { return chunks } for i := range chunks { updated, found := state.applyChunk(ctx, chunks[i]) if !found { continue } state.handled = true chunks[i] = updated break } return chunks } func (state *ClaudeInputTokenState) applyChunk(ctx context.Context, chunk []byte) ([]byte, bool) { for lineStart := 0; lineStart < len(chunk); { lineEnd := bytes.IndexByte(chunk[lineStart:], '\n') if lineEnd < 0 { lineEnd = len(chunk) } else { lineEnd += lineStart } contentEnd := lineEnd if contentEnd > lineStart && chunk[contentEnd-1] == '\r' { contentEnd-- } line := chunk[lineStart:contentEnd] trimmedLeft := bytes.TrimLeft(line, " \t") if bytes.HasPrefix(trimmedLeft, []byte("data:")) { payloadOffset := len(line) - len(trimmedLeft) + len("data:") for payloadOffset < len(line) && (line[payloadOffset] == ' ' || line[payloadOffset] == '\t') { payloadOffset++ } payloadEnd := len(line) for payloadEnd > payloadOffset && (line[payloadEnd-1] == ' ' || line[payloadEnd-1] == '\t') { payloadEnd-- } payload := line[payloadOffset:payloadEnd] if gjson.GetBytes(payload, "type").String() == "message_start" { inputTokens := gjson.GetBytes(payload, "message.usage.input_tokens") if inputTokens.Exists() && inputTokens.Int() != 0 { return chunk, true } count, err := state.estimate() if err != nil { state.logEstimateError(ctx, err) return chunk, true } if count == 0 { return chunk, true } updatedPayload, errSet := sjson.SetBytes(payload, "message.usage.input_tokens", count) if errSet != nil { state.logEstimateError(ctx, fmt.Errorf("set message_start usage: %w", errSet)) return chunk, true } payloadStart := lineStart + payloadOffset payloadStop := lineStart + payloadEnd updated := make([]byte, 0, len(chunk)+len(updatedPayload)-len(payload)) updated = append(updated, chunk[:payloadStart]...) updated = append(updated, updatedPayload...) updated = append(updated, chunk[payloadStop:]...) return updated, true } } if lineEnd == len(chunk) { break } lineStart = lineEnd + 1 } return chunk, false } func (state *ClaudeInputTokenState) estimate() (int64, error) { enc := state.codec if enc == nil { var err error enc, err = claudeInputTokenizer() if err != nil { return 0, fmt.Errorf("initialize O200kBase tokenizer: %w", err) } } count, err := countClaudeInputTokens(enc, state.originalRequest) if err != nil { return 0, fmt.Errorf("count Claude input tokens: %w", err) } return count, nil } func (state *ClaudeInputTokenState) logEstimateError(ctx context.Context, err error) { LogWithRequestID(ctx).WithFields(log.Fields{ "upstream_format": state.upstreamFormat.String(), "response_format": state.responseFormat.String(), }).WithError(err).Warn("failed to estimate Claude input tokens") }