mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
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:
@@ -82,6 +82,43 @@ func (h *ClaudeCodeAPIHandler) ClaudeMessages(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ClaudeMessages handles Claude-compatible streaming chat completions.
|
||||
// This function implements a sophisticated client rotation and quota management system
|
||||
// to ensure high availability and optimal resource utilization across multiple backend clients.
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Gin context for the request.
|
||||
func (h *ClaudeCodeAPIHandler) ClaudeCountTokens(c *gin.Context) {
|
||||
// Extract raw JSON data from the incoming request
|
||||
rawJSON, err := c.GetRawData()
|
||||
// If data retrieval fails, return a 400 Bad Request error.
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, handlers.ErrorResponse{
|
||||
Error: handlers.ErrorDetail{
|
||||
Message: fmt.Sprintf("Invalid request: %v", err),
|
||||
Type: "invalid_request_error",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/json")
|
||||
|
||||
alt := h.GetAlt(c)
|
||||
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
|
||||
|
||||
modelName := gjson.GetBytes(rawJSON, "model").String()
|
||||
|
||||
resp, errMsg := h.ExecuteCountWithAuthManager(cliCtx, h.HandlerType(), modelName, rawJSON, alt)
|
||||
if errMsg != nil {
|
||||
h.WriteErrorResponse(c, errMsg)
|
||||
cliCancel(errMsg.Error)
|
||||
return
|
||||
}
|
||||
_, _ = c.Writer.Write(resp)
|
||||
cliCancel()
|
||||
}
|
||||
|
||||
// ClaudeModels handles the Claude models listing endpoint.
|
||||
// It returns a JSON response containing available Claude models and their specifications.
|
||||
//
|
||||
|
||||
@@ -198,6 +198,7 @@ func (s *Server) setupRoutes() {
|
||||
v1.POST("/chat/completions", openaiHandlers.ChatCompletions)
|
||||
v1.POST("/completions", openaiHandlers.Completions)
|
||||
v1.POST("/messages", claudeCodeHandlers.ClaudeMessages)
|
||||
v1.POST("/messages/count_tokens", claudeCodeHandlers.ClaudeCountTokens)
|
||||
v1.POST("/responses", openaiResponsesHandlers.Responses)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -175,7 +176,68 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
||||
}
|
||||
|
||||
func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {
|
||||
return cliproxyexecutor.Response{Payload: []byte{}}, fmt.Errorf("not implemented")
|
||||
apiKey, baseURL := claudeCreds(auth)
|
||||
if apiKey == "" {
|
||||
return NewClientAdapter("claude").Execute(ctx, auth, req, opts)
|
||||
}
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.anthropic.com"
|
||||
}
|
||||
|
||||
from := opts.SourceFormat
|
||||
to := sdktranslator.FromString("claude")
|
||||
// Use streaming translation to preserve function calling, except for claude.
|
||||
stream := from != to
|
||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), stream)
|
||||
|
||||
if !strings.HasPrefix(req.Model, "claude-3-5-haiku") {
|
||||
body, _ = sjson.SetRawBytes(body, "system", []byte(misc.ClaudeCodeInstructions))
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/messages/count_tokens?beta=true", baseURL)
|
||||
recordAPIRequest(ctx, e.cfg, body)
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return cliproxyexecutor.Response{}, err
|
||||
}
|
||||
applyClaudeHeaders(httpReq, apiKey, false)
|
||||
|
||||
httpClient := &http.Client{}
|
||||
if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil {
|
||||
httpClient.Transport = rt
|
||||
}
|
||||
resp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return cliproxyexecutor.Response{}, err
|
||||
}
|
||||
defer func() {
|
||||
if errClose := resp.Body.Close(); errClose != nil {
|
||||
log.Errorf("response body close error: %v", errClose)
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
||||
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)}
|
||||
}
|
||||
reader := io.Reader(resp.Body)
|
||||
var decoder *zstd.Decoder
|
||||
if hasZSTDEcoding(resp.Header.Get("Content-Encoding")) {
|
||||
decoder, err = zstd.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return cliproxyexecutor.Response{}, fmt.Errorf("failed to initialize zstd decoder: %w", err)
|
||||
}
|
||||
reader = decoder
|
||||
defer decoder.Close()
|
||||
}
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return cliproxyexecutor.Response{}, err
|
||||
}
|
||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||
count := gjson.GetBytes(data, "input_tokens").Int()
|
||||
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
||||
return cliproxyexecutor.Response{Payload: []byte(out)}, nil
|
||||
}
|
||||
|
||||
func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||
|
||||
@@ -53,9 +53,11 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
||||
to := sdktranslator.FromString("codex")
|
||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||
|
||||
if util.InArray([]string{"gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) {
|
||||
if util.InArray([]string{"gpt-5", "gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) {
|
||||
body, _ = sjson.SetBytes(body, "model", "gpt-5")
|
||||
switch req.Model {
|
||||
case "gpt-5":
|
||||
body, _ = sjson.DeleteBytes(body, "reasoning.effort")
|
||||
case "gpt-5-minimal":
|
||||
body, _ = sjson.SetBytes(body, "reasoning.effort", "minimal")
|
||||
case "gpt-5-low":
|
||||
@@ -146,9 +148,11 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
||||
to := sdktranslator.FromString("codex")
|
||||
body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true)
|
||||
|
||||
if util.InArray([]string{"gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) {
|
||||
if util.InArray([]string{"gpt-5", "gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) {
|
||||
body, _ = sjson.SetBytes(body, "model", "gpt-5")
|
||||
switch req.Model {
|
||||
case "gpt-5":
|
||||
body, _ = sjson.DeleteBytes(body, "reasoning.effort")
|
||||
case "gpt-5-minimal":
|
||||
body, _ = sjson.SetBytes(body, "reasoning.effort", "minimal")
|
||||
case "gpt-5-low":
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
@@ -156,7 +157,6 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
||||
|
||||
httpClient := newHTTPClient(ctx, 0)
|
||||
respCtx := context.WithValue(ctx, "alt", opts.Alt)
|
||||
dataTag := []byte("data:")
|
||||
|
||||
var lastStatus int
|
||||
var lastBody []byte
|
||||
@@ -321,8 +321,8 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
|
||||
_ = resp.Body.Close()
|
||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||
var param any
|
||||
translated := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, bytes.Clone(opts.OriginalRequest), payload, data, ¶m)
|
||||
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||
translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data)
|
||||
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
|
||||
}
|
||||
lastStatus = resp.StatusCode
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
@@ -182,9 +183,11 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
||||
translatedReq := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false)
|
||||
respCtx := context.WithValue(ctx, "alt", opts.Alt)
|
||||
translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")
|
||||
translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")
|
||||
|
||||
url := fmt.Sprintf("%s/%s/models/%s:%s", glEndpoint, glAPIVersion, req.Model, "countTokens")
|
||||
recordAPIRequest(ctx, e.cfg, translatedReq)
|
||||
|
||||
requestBody := bytes.NewReader(translatedReq)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, requestBody)
|
||||
@@ -218,8 +221,8 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
||||
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(data)}
|
||||
}
|
||||
|
||||
var param any
|
||||
translated := sdktranslator.TranslateNonStream(respCtx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), translatedReq, data, ¶m)
|
||||
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||
translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data)
|
||||
return cliproxyexecutor.Response{Payload: []byte(translated)}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ func init() {
|
||||
Claude,
|
||||
ConvertGeminiCLIRequestToClaude,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertClaudeResponseToGeminiCLI,
|
||||
NonStream: ConvertClaudeResponseToGeminiCLINonStream,
|
||||
Stream: ConvertClaudeResponseToGeminiCLI,
|
||||
NonStream: ConvertClaudeResponseToGeminiCLINonStream,
|
||||
TokenCount: GeminiCLITokenCount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -12,8 +12,9 @@ func init() {
|
||||
Claude,
|
||||
ConvertGeminiRequestToClaude,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertClaudeResponseToGemini,
|
||||
NonStream: ConvertClaudeResponseToGeminiNonStream,
|
||||
Stream: ConvertClaudeResponseToGemini,
|
||||
NonStream: ConvertClaudeResponseToGeminiNonStream,
|
||||
TokenCount: GeminiTokenCount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ func init() {
|
||||
GeminiCLI,
|
||||
ConvertClaudeRequestToCLI,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertGeminiCLIResponseToClaude,
|
||||
NonStream: ConvertGeminiCLIResponseToClaudeNonStream,
|
||||
Stream: ConvertGeminiCLIResponseToClaude,
|
||||
NonStream: ConvertGeminiCLIResponseToClaudeNonStream,
|
||||
TokenCount: ClaudeTokenCount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ func init() {
|
||||
GeminiCLI,
|
||||
ConvertGeminiRequestToGeminiCLI,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertGeminiCliRequestToGemini,
|
||||
NonStream: ConvertGeminiCliRequestToGeminiNonStream,
|
||||
Stream: ConvertGeminiCliRequestToGemini,
|
||||
NonStream: ConvertGeminiCliRequestToGeminiNonStream,
|
||||
TokenCount: GeminiTokenCount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ func init() {
|
||||
Gemini,
|
||||
ConvertClaudeRequestToGemini,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertGeminiResponseToClaude,
|
||||
NonStream: ConvertGeminiResponseToClaudeNonStream,
|
||||
Stream: ConvertGeminiResponseToClaude,
|
||||
NonStream: ConvertGeminiResponseToClaudeNonStream,
|
||||
TokenCount: ClaudeTokenCount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ func init() {
|
||||
Gemini,
|
||||
ConvertGeminiCLIRequestToGemini,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertGeminiResponseToGeminiCLI,
|
||||
NonStream: ConvertGeminiResponseToGeminiCLINonStream,
|
||||
Stream: ConvertGeminiResponseToGeminiCLI,
|
||||
NonStream: ConvertGeminiResponseToGeminiCLINonStream,
|
||||
TokenCount: GeminiCLITokenCount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ func init() {
|
||||
Gemini,
|
||||
ConvertGeminiRequestToGemini,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: PassthroughGeminiResponseStream,
|
||||
NonStream: PassthroughGeminiResponseNonStream,
|
||||
Stream: PassthroughGeminiResponseStream,
|
||||
NonStream: PassthroughGeminiResponseNonStream,
|
||||
TokenCount: GeminiTokenCount,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,19 @@ func (r *Registry) TranslateNonStream(ctx context.Context, from, to Format, mode
|
||||
return string(rawJSON)
|
||||
}
|
||||
|
||||
// TranslateNonStream applies the registered non-stream response translator.
|
||||
func (r *Registry) TranslateTokenCount(ctx context.Context, from, to Format, count int64, rawJSON []byte) string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if byTarget, ok := r.responses[to]; ok {
|
||||
if fn, isOk := byTarget[from]; isOk && fn.TokenCount != nil {
|
||||
return fn.TokenCount(ctx, count)
|
||||
}
|
||||
}
|
||||
return string(rawJSON)
|
||||
}
|
||||
|
||||
var defaultRegistry = NewRegistry()
|
||||
|
||||
// Default exposes the package-level registry for shared use.
|
||||
@@ -122,3 +135,8 @@ func TranslateStream(ctx context.Context, from, to Format, model string, origina
|
||||
func TranslateNonStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string {
|
||||
return defaultRegistry.TranslateNonStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||
}
|
||||
|
||||
// TranslateTokenCount is a helper on the default registry.
|
||||
func TranslateTokenCount(ctx context.Context, from, to Format, count int64, rawJSON []byte) string {
|
||||
return defaultRegistry.TranslateTokenCount(ctx, from, to, count, rawJSON)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ type ResponseStreamTransform func(ctx context.Context, model string, originalReq
|
||||
// ResponseNonStreamTransform converts non-stream responses between schemas.
|
||||
type ResponseNonStreamTransform func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string
|
||||
|
||||
type ResponseTokenCountTransform func(ctx context.Context, count int64) string
|
||||
|
||||
// ResponseTransform groups streaming and non-streaming transforms.
|
||||
type ResponseTransform struct {
|
||||
Stream ResponseStreamTransform
|
||||
NonStream ResponseNonStreamTransform
|
||||
Stream ResponseStreamTransform
|
||||
NonStream ResponseNonStreamTransform
|
||||
TokenCount ResponseTokenCountTransform
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user