feat(translators): add token counting support for Claude and Gemini responses

- Implemented `TokenCount` transform method across translators to calculate token usage.
- Integrated token counting logic into executor pipelines for Claude, Gemini, and CLI translators.
- Added corresponding API endpoints and handlers (`/messages/count_tokens`) for token usage retrieval.
- Enhanced translation registry to support `TokenCount` functionality alongside existing response types.
This commit is contained in:
Luis Pater
2025-09-24 11:59:38 +08:00
parent 582677d067
commit 3dd5095792
22 changed files with 192 additions and 25 deletions

View File

@@ -54,5 +54,8 @@ func ConvertClaudeResponseToGeminiCLINonStream(ctx context.Context, modelName st
json := `{"response": {}}`
strJSON, _ = sjson.SetRaw(json, "response", strJSON)
return strJSON
}
func GeminiCLITokenCount(ctx context.Context, count int64) string {
return GeminiTokenCount(ctx, count)
}

View File

@@ -12,8 +12,9 @@ func init() {
Claude,
ConvertGeminiCLIRequestToClaude,
interfaces.TranslateResponse{
Stream: ConvertClaudeResponseToGeminiCLI,
NonStream: ConvertClaudeResponseToGeminiCLINonStream,
Stream: ConvertClaudeResponseToGeminiCLI,
NonStream: ConvertClaudeResponseToGeminiCLINonStream,
TokenCount: GeminiCLITokenCount,
},
)
}

View File

@@ -9,6 +9,7 @@ import (
"bufio"
"bytes"
"context"
"fmt"
"strings"
"time"
@@ -530,6 +531,10 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
return template
}
func GeminiTokenCount(ctx context.Context, count int64) string {
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
}
// consolidateParts merges consecutive text parts and thinking parts to create a cleaner response.
// This function processes the parts array to combine adjacent text elements and thinking elements
// into single consolidated parts, which results in a more readable and efficient response structure.

View File

@@ -12,8 +12,9 @@ func init() {
Claude,
ConvertGeminiRequestToClaude,
interfaces.TranslateResponse{
Stream: ConvertClaudeResponseToGemini,
NonStream: ConvertClaudeResponseToGeminiNonStream,
Stream: ConvertClaudeResponseToGemini,
NonStream: ConvertClaudeResponseToGeminiNonStream,
TokenCount: GeminiTokenCount,
},
)
}

View File

@@ -376,3 +376,7 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
}
return string(encoded)
}
func ClaudeTokenCount(ctx context.Context, count int64) string {
return fmt.Sprintf(`{"input_tokens":%d}`, count)
}

View File

@@ -12,8 +12,9 @@ func init() {
GeminiCLI,
ConvertClaudeRequestToCLI,
interfaces.TranslateResponse{
Stream: ConvertGeminiCLIResponseToClaude,
NonStream: ConvertGeminiCLIResponseToClaudeNonStream,
Stream: ConvertGeminiCLIResponseToClaude,
NonStream: ConvertGeminiCLIResponseToClaudeNonStream,
TokenCount: ClaudeTokenCount,
},
)
}

View File

@@ -7,6 +7,7 @@ package gemini
import (
"context"
"fmt"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -74,3 +75,7 @@ func ConvertGeminiCliRequestToGeminiNonStream(_ context.Context, _ string, origi
}
return string(rawJSON)
}
func GeminiTokenCount(ctx context.Context, count int64) string {
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
}

View File

@@ -12,8 +12,9 @@ func init() {
GeminiCLI,
ConvertGeminiRequestToGeminiCLI,
interfaces.TranslateResponse{
Stream: ConvertGeminiCliRequestToGemini,
NonStream: ConvertGeminiCliRequestToGeminiNonStream,
Stream: ConvertGeminiCliRequestToGemini,
NonStream: ConvertGeminiCliRequestToGeminiNonStream,
TokenCount: GeminiTokenCount,
},
)
}

View File

@@ -370,3 +370,7 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
}
return string(encoded)
}
func ClaudeTokenCount(ctx context.Context, count int64) string {
return fmt.Sprintf(`{"input_tokens":%d}`, count)
}

View File

@@ -12,8 +12,9 @@ func init() {
Gemini,
ConvertClaudeRequestToGemini,
interfaces.TranslateResponse{
Stream: ConvertGeminiResponseToClaude,
NonStream: ConvertGeminiResponseToClaudeNonStream,
Stream: ConvertGeminiResponseToClaude,
NonStream: ConvertGeminiResponseToClaudeNonStream,
TokenCount: ClaudeTokenCount,
},
)
}

View File

@@ -7,6 +7,8 @@ package geminiCLI
import (
"bytes"
"context"
"fmt"
"github.com/tidwall/sjson"
)
@@ -47,3 +49,7 @@ func ConvertGeminiResponseToGeminiCLINonStream(_ context.Context, _ string, orig
rawJSON, _ = sjson.SetRawBytes([]byte(json), "response", rawJSON)
return string(rawJSON)
}
func GeminiCLITokenCount(ctx context.Context, count int64) string {
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
}

View File

@@ -12,8 +12,9 @@ func init() {
Gemini,
ConvertGeminiCLIRequestToGemini,
interfaces.TranslateResponse{
Stream: ConvertGeminiResponseToGeminiCLI,
NonStream: ConvertGeminiResponseToGeminiCLINonStream,
Stream: ConvertGeminiResponseToGeminiCLI,
NonStream: ConvertGeminiResponseToGeminiCLINonStream,
TokenCount: GeminiCLITokenCount,
},
)
}

View File

@@ -3,6 +3,7 @@ package gemini
import (
"bytes"
"context"
"fmt"
)
// PassthroughGeminiResponseStream forwards Gemini responses unchanged.
@@ -22,3 +23,7 @@ func PassthroughGeminiResponseStream(_ context.Context, _ string, originalReques
func PassthroughGeminiResponseNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
return string(rawJSON)
}
func GeminiTokenCount(ctx context.Context, count int64) string {
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
}

View File

@@ -14,8 +14,9 @@ func init() {
Gemini,
ConvertGeminiRequestToGemini,
interfaces.TranslateResponse{
Stream: PassthroughGeminiResponseStream,
NonStream: PassthroughGeminiResponseNonStream,
Stream: PassthroughGeminiResponseStream,
NonStream: PassthroughGeminiResponseNonStream,
TokenCount: GeminiTokenCount,
},
)
}