Refactor codebase

This commit is contained in:
Luis Pater
2025-08-22 01:31:12 +08:00
parent 2b1762be16
commit 8c555c4e69
109 changed files with 7319 additions and 5735 deletions

View File

@@ -1,3 +1,6 @@
// Package client provides HTTP client functionality for interacting with Anthropic's Claude API.
// It handles authentication, request/response translation, streaming communication,
// and quota management for Claude models.
package client
import (
@@ -17,7 +20,10 @@ import (
"github.com/luispater/CLIProxyAPI/internal/auth/claude"
"github.com/luispater/CLIProxyAPI/internal/auth/empty"
"github.com/luispater/CLIProxyAPI/internal/config"
. "github.com/luispater/CLIProxyAPI/internal/constant"
"github.com/luispater/CLIProxyAPI/internal/interfaces"
"github.com/luispater/CLIProxyAPI/internal/misc"
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
"github.com/luispater/CLIProxyAPI/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
@@ -28,14 +34,25 @@ const (
claudeEndpoint = "https://api.anthropic.com"
)
// ClaudeClient implements the Client interface for OpenAI API
// ClaudeClient implements the Client interface for Anthropic's Claude API.
// It provides methods for authenticating with Claude and sending requests to Claude models.
type ClaudeClient struct {
ClientBase
claudeAuth *claude.ClaudeAuth
// claudeAuth handles authentication with Claude API
claudeAuth *claude.ClaudeAuth
// apiKeyIndex is the index of the API key to use from the config, -1 if not using API keys
apiKeyIndex int
}
// NewClaudeClient creates a new OpenAI client instance
// NewClaudeClient creates a new Claude client instance using token-based authentication.
// It initializes the client with the provided configuration and token storage.
//
// Parameters:
// - cfg: The application configuration.
// - ts: The token storage for Claude authentication.
//
// Returns:
// - *ClaudeClient: A new Claude client instance.
func NewClaudeClient(cfg *config.Config, ts *claude.ClaudeTokenStorage) *ClaudeClient {
httpClient := util.SetProxy(cfg, &http.Client{})
client := &ClaudeClient{
@@ -53,7 +70,16 @@ func NewClaudeClient(cfg *config.Config, ts *claude.ClaudeTokenStorage) *ClaudeC
return client
}
// NewClaudeClientWithKey creates a new OpenAI client instance with api key
// NewClaudeClientWithKey creates a new Claude client instance using API key authentication.
// It initializes the client with the provided configuration and selects the API key
// at the specified index from the configuration.
//
// Parameters:
// - cfg: The application configuration.
// - apiKeyIndex: The index of the API key to use from the configuration.
//
// Returns:
// - *ClaudeClient: A new Claude client instance.
func NewClaudeClientWithKey(cfg *config.Config, apiKeyIndex int) *ClaudeClient {
httpClient := util.SetProxy(cfg, &http.Client{})
client := &ClaudeClient{
@@ -71,7 +97,41 @@ func NewClaudeClientWithKey(cfg *config.Config, apiKeyIndex int) *ClaudeClient {
return client
}
// GetAPIKey returns the api key index
// Type returns the client type identifier.
// This method returns "claude" to identify this client as a Claude API client.
func (c *ClaudeClient) Type() string {
return CLAUDE
}
// Provider returns the provider name for this client.
// This method returns "claude" to identify Anthropic's Claude as the provider.
func (c *ClaudeClient) Provider() string {
return CLAUDE
}
// CanProvideModel checks if this client can provide the specified model.
// It returns true if the model is supported by Claude, false otherwise.
//
// Parameters:
// - modelName: The name of the model to check.
//
// Returns:
// - bool: True if the model is supported, false otherwise.
func (c *ClaudeClient) CanProvideModel(modelName string) bool {
// List of Claude models supported by this client
models := []string{
"claude-opus-4-1-20250805",
"claude-opus-4-20250514",
"claude-sonnet-4-20250514",
"claude-3-7-sonnet-20250219",
"claude-3-5-haiku-20241022",
}
return util.InArray(models, modelName)
}
// GetAPIKey returns the API key for Claude API requests.
// If an API key index is specified, it returns the corresponding key from the configuration.
// Otherwise, it returns an empty string, indicating token-based authentication should be used.
func (c *ClaudeClient) GetAPIKey() string {
if c.apiKeyIndex != -1 {
return c.cfg.ClaudeKey[c.apiKeyIndex].APIKey
@@ -79,43 +139,37 @@ func (c *ClaudeClient) GetAPIKey() string {
return ""
}
// GetUserAgent returns the user agent string for OpenAI API requests
// GetUserAgent returns the user agent string for Claude API requests.
// This identifies the client as the Claude CLI to the Anthropic API.
func (c *ClaudeClient) GetUserAgent() string {
return "claude-cli/1.0.83 (external, cli)"
}
// TokenStorage returns the token storage interface used by this client.
// This provides access to the authentication token management system.
func (c *ClaudeClient) TokenStorage() auth.TokenStorage {
return c.tokenStorage
}
// SendMessage sends a message to OpenAI API (non-streaming)
func (c *ClaudeClient) SendMessage(_ context.Context, _ []byte, _ string, _ *Content, _ []Content, _ []ToolDeclaration) ([]byte, *ErrorMessage) {
// For now, return an error as OpenAI integration is not fully implemented
return nil, &ErrorMessage{
StatusCode: http.StatusNotImplemented,
Error: fmt.Errorf("claude message sending not yet implemented"),
}
}
// SendRawMessage sends a raw message to Claude API and returns the response.
// It handles request translation, API communication, error handling, and response translation.
//
// Parameters:
// - ctx: The context for the request.
// - modelName: The name of the model to use.
// - rawJSON: The raw JSON request body.
// - alt: An alternative response format parameter.
//
// Returns:
// - []byte: The response body.
// - *interfaces.ErrorMessage: An error message if the request fails.
func (c *ClaudeClient) SendRawMessage(ctx context.Context, modelName string, rawJSON []byte, alt string) ([]byte, *interfaces.ErrorMessage) {
handler := ctx.Value("handler").(interfaces.APIHandler)
handlerType := handler.HandlerType()
rawJSON = translator.Request(handlerType, c.Type(), modelName, rawJSON, false)
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
// SendMessageStream sends a streaming message to OpenAI API
func (c *ClaudeClient) SendMessageStream(_ context.Context, _ []byte, _ string, _ *Content, _ []Content, _ []ToolDeclaration, _ ...bool) (<-chan []byte, <-chan *ErrorMessage) {
errChan := make(chan *ErrorMessage, 1)
errChan <- &ErrorMessage{
StatusCode: http.StatusNotImplemented,
Error: fmt.Errorf("claude streaming not yet implemented"),
}
close(errChan)
return nil, errChan
}
// SendRawMessage sends a raw message to OpenAI API
func (c *ClaudeClient) SendRawMessage(ctx context.Context, rawJSON []byte, alt string) ([]byte, *ErrorMessage) {
modelResult := gjson.GetBytes(rawJSON, "model")
model := modelResult.String()
modelName := model
respBody, err := c.APIRequest(ctx, "/v1/messages?beta=true", rawJSON, alt, false)
respBody, err := c.APIRequest(ctx, modelName, "/v1/messages?beta=true", rawJSON, alt, false)
if err != nil {
if err.StatusCode == 429 {
now := time.Now()
@@ -126,50 +180,88 @@ func (c *ClaudeClient) SendRawMessage(ctx context.Context, rawJSON []byte, alt s
delete(c.modelQuotaExceeded, modelName)
bodyBytes, errReadAll := io.ReadAll(respBody)
if errReadAll != nil {
return nil, &ErrorMessage{StatusCode: 500, Error: errReadAll}
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: errReadAll}
}
return bodyBytes, nil
c.AddAPIResponseData(ctx, bodyBytes)
var param any
bodyBytes = []byte(translator.ResponseNonStream(handlerType, c.Type(), ctx, modelName, bodyBytes, &param))
return bodyBytes, nil
}
// SendRawMessageStream sends a raw streaming message to OpenAI API
func (c *ClaudeClient) SendRawMessageStream(ctx context.Context, rawJSON []byte, alt string) (<-chan []byte, <-chan *ErrorMessage) {
errChan := make(chan *ErrorMessage)
// SendRawMessageStream sends a raw streaming message to Claude API.
// It returns two channels: one for receiving response data chunks and one for errors.
//
// Parameters:
// - ctx: The context for the request.
// - modelName: The name of the model to use.
// - rawJSON: The raw JSON request body.
// - alt: An alternative response format parameter.
//
// Returns:
// - <-chan []byte: A channel for receiving response data chunks.
// - <-chan *interfaces.ErrorMessage: A channel for receiving error messages.
func (c *ClaudeClient) SendRawMessageStream(ctx context.Context, modelName string, rawJSON []byte, alt string) (<-chan []byte, <-chan *interfaces.ErrorMessage) {
handler := ctx.Value("handler").(interfaces.APIHandler)
handlerType := handler.HandlerType()
rawJSON = translator.Request(handlerType, c.Type(), modelName, rawJSON, true)
errChan := make(chan *interfaces.ErrorMessage)
dataChan := make(chan []byte)
// log.Debugf(string(rawJSON))
// return dataChan, errChan
go func() {
defer close(errChan)
defer close(dataChan)
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
modelResult := gjson.GetBytes(rawJSON, "model")
model := modelResult.String()
modelName := model
var stream io.ReadCloser
for {
var err *ErrorMessage
stream, err = c.APIRequest(ctx, "/v1/messages?beta=true", rawJSON, alt, true)
if err != nil {
if err.StatusCode == 429 {
now := time.Now()
c.modelQuotaExceeded[modelName] = &now
}
errChan <- err
return
if c.IsModelQuotaExceeded(modelName) {
errChan <- &interfaces.ErrorMessage{
StatusCode: 429,
Error: fmt.Errorf(`{"error":{"code":429,"message":"All the models of '%s' are quota exceeded","status":"RESOURCE_EXHAUSTED"}}`, modelName),
}
delete(c.modelQuotaExceeded, modelName)
break
return
}
var err *interfaces.ErrorMessage
stream, err = c.APIRequest(ctx, modelName, "/v1/messages?beta=true", rawJSON, alt, true)
if err != nil {
if err.StatusCode == 429 {
now := time.Now()
c.modelQuotaExceeded[modelName] = &now
}
errChan <- err
return
}
delete(c.modelQuotaExceeded, modelName)
scanner := bufio.NewScanner(stream)
buffer := make([]byte, 10240*1024)
scanner.Buffer(buffer, 10240*1024)
for scanner.Scan() {
line := scanner.Bytes()
dataChan <- line
if translator.NeedConvert(handlerType, c.Type()) {
var param any
for scanner.Scan() {
line := scanner.Bytes()
lines := translator.Response(handlerType, c.Type(), ctx, modelName, line, &param)
for i := 0; i < len(lines); i++ {
dataChan <- []byte(lines[i])
}
c.AddAPIResponseData(ctx, line)
}
} else {
for scanner.Scan() {
line := scanner.Bytes()
dataChan <- line
c.AddAPIResponseData(ctx, line)
}
}
if errScanner := scanner.Err(); errScanner != nil {
errChan <- &ErrorMessage{500, errScanner, nil}
errChan <- &interfaces.ErrorMessage{StatusCode: 500, Error: errScanner}
_ = stream.Close()
return
}
@@ -180,36 +272,62 @@ func (c *ClaudeClient) SendRawMessageStream(ctx context.Context, rawJSON []byte,
return dataChan, errChan
}
// SendRawTokenCount sends a token count request to OpenAI API
func (c *ClaudeClient) SendRawTokenCount(_ context.Context, _ []byte, _ string) ([]byte, *ErrorMessage) {
return nil, &ErrorMessage{
// SendRawTokenCount sends a token count request to Claude API.
// Currently, this functionality is not implemented for Claude models.
// It returns a NotImplemented error.
//
// Parameters:
// - ctx: The context for the request.
// - modelName: The name of the model to use.
// - rawJSON: The raw JSON request body.
// - alt: An alternative response format parameter.
//
// Returns:
// - []byte: Always nil for this implementation.
// - *interfaces.ErrorMessage: An error message indicating that the feature is not implemented.
func (c *ClaudeClient) SendRawTokenCount(_ context.Context, _ string, _ []byte, _ string) ([]byte, *interfaces.ErrorMessage) {
return nil, &interfaces.ErrorMessage{
StatusCode: http.StatusNotImplemented,
Error: fmt.Errorf("claude token counting not yet implemented"),
}
}
// SaveTokenToFile persists the token storage to disk
// SaveTokenToFile persists the authentication tokens to disk.
// It saves the token data to a JSON file in the configured authentication directory,
// with a filename based on the user's email address.
//
// Returns:
// - error: An error if the save operation fails, nil otherwise.
func (c *ClaudeClient) SaveTokenToFile() error {
fileName := filepath.Join(c.cfg.AuthDir, fmt.Sprintf("claude-%s.json", c.tokenStorage.(*claude.ClaudeTokenStorage).Email))
return c.tokenStorage.SaveTokenToFile(fileName)
}
// RefreshTokens refreshes the access tokens if needed
// RefreshTokens refreshes the access tokens if they have expired.
// It uses the refresh token to obtain new access tokens from the Claude authentication service.
// If successful, it updates the token storage and persists the new tokens to disk.
//
// Parameters:
// - ctx: The context for the request.
//
// Returns:
// - error: An error if the refresh operation fails, nil otherwise.
func (c *ClaudeClient) RefreshTokens(ctx context.Context) error {
// Check if we have a valid refresh token
if c.tokenStorage == nil || c.tokenStorage.(*claude.ClaudeTokenStorage).RefreshToken == "" {
return fmt.Errorf("no refresh token available")
}
// Refresh tokens using the auth service
// Refresh tokens using the auth service with retry mechanism
newTokenData, err := c.claudeAuth.RefreshTokensWithRetry(ctx, c.tokenStorage.(*claude.ClaudeTokenStorage).RefreshToken, 3)
if err != nil {
return fmt.Errorf("failed to refresh tokens: %w", err)
}
// Update token storage
// Update token storage with new token data
c.claudeAuth.UpdateTokenStorage(c.tokenStorage.(*claude.ClaudeTokenStorage), newTokenData)
// Save updated tokens
// Save updated tokens to persistent storage
if err = c.SaveTokenToFile(); err != nil {
log.Warnf("Failed to save refreshed tokens: %v", err)
}
@@ -218,16 +336,30 @@ func (c *ClaudeClient) RefreshTokens(ctx context.Context) error {
return nil
}
// APIRequest handles making requests to the CLI API endpoints.
func (c *ClaudeClient) APIRequest(ctx context.Context, endpoint string, body interface{}, _ string, _ bool) (io.ReadCloser, *ErrorMessage) {
// APIRequest handles making HTTP requests to the Claude API endpoints.
// It manages authentication, request preparation, and response handling.
//
// Parameters:
// - ctx: The context for the request, which may contain additional request metadata.
// - modelName: The name of the model being requested.
// - endpoint: The API endpoint path to call (e.g., "/v1/messages").
// - body: The request body, either as a byte array or an object to be marshaled to JSON.
// - alt: An alternative response format parameter (unused in this implementation).
// - stream: A boolean indicating if the request is for a streaming response (unused in this implementation).
//
// Returns:
// - io.ReadCloser: The response body reader if successful.
// - *interfaces.ErrorMessage: Error information if the request fails.
func (c *ClaudeClient) APIRequest(ctx context.Context, modelName, endpoint string, body interface{}, _ string, _ bool) (io.ReadCloser, *interfaces.ErrorMessage) {
var jsonBody []byte
var err error
// Convert body to JSON bytes
if byteBody, ok := body.([]byte); ok {
jsonBody = byteBody
} else {
jsonBody, err = json.Marshal(body)
if err != nil {
return nil, &ErrorMessage{500, fmt.Errorf("failed to marshal request body: %w", err), nil}
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: fmt.Errorf("failed to marshal request body: %w", err)}
}
}
@@ -268,7 +400,7 @@ func (c *ClaudeClient) APIRequest(ctx context.Context, endpoint string, body int
req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody)
if err != nil {
return nil, &ErrorMessage{500, fmt.Errorf("failed to create request: %v", err), nil}
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: fmt.Errorf("failed to create request: %v", err)}
}
// Set headers
@@ -294,13 +426,21 @@ func (c *ClaudeClient) APIRequest(ctx context.Context, endpoint string, body int
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
req.Header.Set("Anthropic-Beta", "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14")
if ginContext, ok := ctx.Value("gin").(*gin.Context); ok {
ginContext.Set("API_REQUEST", jsonBody)
if c.cfg.RequestLog {
if ginContext, ok := ctx.Value("gin").(*gin.Context); ok {
ginContext.Set("API_REQUEST", jsonBody)
}
}
if c.apiKeyIndex != -1 {
log.Debugf("Use Claude API key %s for model %s", util.HideAPIKey(c.cfg.ClaudeKey[c.apiKeyIndex].APIKey), modelName)
} else {
log.Debugf("Use Claude account %s for model %s", c.GetEmail(), modelName)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, &ErrorMessage{500, fmt.Errorf("failed to execute request: %v", err), nil}
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: fmt.Errorf("failed to execute request: %v", err)}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
@@ -314,12 +454,20 @@ func (c *ClaudeClient) APIRequest(ctx context.Context, endpoint string, body int
addon := c.createAddon(resp.Header)
// log.Debug(string(jsonBody))
return nil, &ErrorMessage{resp.StatusCode, fmt.Errorf(string(bodyBytes)), addon}
return nil, &interfaces.ErrorMessage{StatusCode: resp.StatusCode, Error: fmt.Errorf("%s", string(bodyBytes)), Addon: addon}
}
return resp.Body, nil
}
// createAddon creates a new http.Header containing selected headers from the original response.
// This is used to pass relevant rate limit and retry information back to the caller.
//
// Parameters:
// - header: The original http.Header from the API response.
//
// Returns:
// - http.Header: A new header containing the selected headers.
func (c *ClaudeClient) createAddon(header http.Header) http.Header {
addon := http.Header{}
if _, ok := header["X-Should-Retry"]; ok {
@@ -352,6 +500,8 @@ func (c *ClaudeClient) createAddon(header http.Header) http.Header {
return addon
}
// GetEmail returns the email address associated with the client's token storage.
// If the client is using API key authentication, it returns an empty string.
func (c *ClaudeClient) GetEmail() string {
if ts, ok := c.tokenStorage.(*claude.ClaudeTokenStorage); ok {
return ts.Email
@@ -362,6 +512,12 @@ func (c *ClaudeClient) GetEmail() string {
// IsModelQuotaExceeded returns true if the specified model has exceeded its quota
// and no fallback options are available.
//
// Parameters:
// - model: The name of the model to check.
//
// Returns:
// - bool: True if the model's quota is exceeded, false otherwise.
func (c *ClaudeClient) IsModelQuotaExceeded(model string) bool {
if lastExceededTime, hasKey := c.modelQuotaExceeded[model]; hasKey {
duration := time.Now().Sub(*lastExceededTime)