claude-code-proxy/proxy/internal/service/anthropic.go
Seif Ghazi bd126e3d8a
feat: implement proxy v2 with backend and frontend enhancements
- Enhanced proxy handlers and Anthropic service integration
- Improved SQLite storage and configuration
- Updated web UI and request handling

temp

WIP: additional changes
2025-07-09 12:57:10 -04:00

122 lines
3.3 KiB
Go

package service
import (
"compress/gzip"
"context"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/seifghazi/claude-code-monitor/internal/config"
)
type AnthropicService interface {
ForwardRequest(ctx context.Context, originalReq *http.Request) (*http.Response, error)
}
type anthropicService struct {
client *http.Client
config *config.AnthropicConfig
}
func NewAnthropicService(cfg *config.AnthropicConfig) AnthropicService {
return &anthropicService{
client: &http.Client{
Timeout: 300 * time.Second, // Increased timeout to 5 minutes
},
config: cfg,
}
}
func (s *anthropicService) ForwardRequest(ctx context.Context, originalReq *http.Request) (*http.Response, error) {
// Clone the request to avoid modifying the original
proxyReq := originalReq.Clone(ctx)
// Parse the configured base URL
baseURL, err := url.Parse(s.config.BaseURL)
if err != nil {
return nil, fmt.Errorf("failed to parse base URL '%s': %w", s.config.BaseURL, err)
}
if baseURL.Scheme == "" || baseURL.Host == "" {
return nil, fmt.Errorf("invalid base URL, scheme and host are required: %s", s.config.BaseURL)
}
// Update the destination URL
proxyReq.URL.Scheme = baseURL.Scheme
proxyReq.URL.Host = baseURL.Host
proxyReq.URL.Path = path.Join(baseURL.Path, "/v1/messages")
// Preserve query parameters from original request
proxyReq.URL.RawQuery = originalReq.URL.RawQuery
// Clear fields that can't be set in client requests
proxyReq.RequestURI = "" // This is set by the server and must be cleared
proxyReq.Host = "" // Let Go set this from the URL
// Forward the request with all original headers intact
resp, err := s.client.Do(proxyReq)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
// Handle gzip decompression
if strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") {
decompressedResp, err := s.decompressGzipResponse(resp)
if err != nil {
resp.Body.Close()
return nil, fmt.Errorf("failed to decompress gzip response: %w", err)
}
return decompressedResp, nil
}
return resp, nil
}
func (s *anthropicService) decompressGzipResponse(resp *http.Response) (*http.Response, error) {
// Create a gzip reader
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
}
// Read the decompressed data
decompressedData, err := io.ReadAll(gzipReader)
if err != nil {
gzipReader.Close()
return nil, fmt.Errorf("failed to read decompressed data: %w", err)
}
// Close the gzip reader and original body
gzipReader.Close()
resp.Body.Close()
// Create a new response with decompressed body
newResp := &http.Response{
Status: resp.Status,
StatusCode: resp.StatusCode,
Proto: resp.Proto,
ProtoMajor: resp.ProtoMajor,
ProtoMinor: resp.ProtoMinor,
Header: resp.Header.Clone(),
ContentLength: int64(len(decompressedData)),
TransferEncoding: resp.TransferEncoding,
Close: resp.Close,
Uncompressed: true,
Trailer: resp.Trailer,
Request: resp.Request,
TLS: resp.TLS,
}
// Remove Content-Encoding header since we've decompressed
newResp.Header.Del("Content-Encoding")
// Set the decompressed body
newResp.Body = io.NopCloser(strings.NewReader(string(decompressedData)))
return newResp, nil
}