-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxy.go
More file actions
136 lines (116 loc) · 3.94 KB
/
proxy.go
File metadata and controls
136 lines (116 loc) · 3.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package llmproxy
import (
"bytes"
"context"
"io"
"net/http"
)
// Proxy forwards requests to an upstream LLM provider.
//
// Proxy handles the complete request lifecycle:
// 1. Reads and parses the request body
// 2. Resolves the upstream URL
// 3. Creates and enriches the upstream request
// 4. Executes the request through the interceptor chain
// 5. Extracts metadata from the response
// 6. Re-attaches the raw response body
//
// Use NewProxy with functional options to configure:
//
// proxy := NewProxy(provider,
// WithInterceptor(loggingInterceptor),
// WithHTTPClient(customClient),
// )
type Proxy struct {
provider Provider
globalInterceptors InterceptorChain
client *http.Client
}
// ProxyOption configures a Proxy during construction.
type ProxyOption func(*Proxy)
// WithInterceptor adds an interceptor to the global chain.
// Interceptors are applied in the order they are added.
func WithInterceptor(i Interceptor) ProxyOption {
return func(p *Proxy) { p.globalInterceptors = append(p.globalInterceptors, i) }
}
// WithHTTPClient sets a custom HTTP client for upstream requests.
// If not set, http.DefaultClient is used.
func WithHTTPClient(c *http.Client) ProxyOption {
return func(p *Proxy) { p.client = c }
}
// NewProxy creates a new proxy for the given provider.
// Options can be used to add interceptors or customize the HTTP client.
func NewProxy(provider Provider, opts ...ProxyOption) *Proxy {
p := &Proxy{
provider: provider,
client: http.DefaultClient,
}
for _, opt := range opts {
opt(p)
}
return p
}
// Forward sends a request to the upstream provider and returns the response.
//
// The method:
// 1. Reads and parses the request body to extract metadata
// 2. Resolves the upstream URL based on the metadata
// 3. Creates a new request for the upstream, copying headers
// 4. Enriches the request with provider-specific headers
// 5. Executes the request through the interceptor chain
// 6. Extracts metadata from the response
// 7. Re-attaches the raw response body so the caller can read it
//
// The returned response body contains the original raw bytes from the upstream
// and can be read by the caller. Any custom/unsupported fields in the JSON
// are preserved.
func (p *Proxy) Forward(ctx context.Context, req *http.Request) (*http.Response, ResponseMetadata, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, ResponseMetadata{}, err
}
req.Body.Close()
meta, _, err := p.provider.BodyParser().Parse(io.NopCloser(bytes.NewReader(body)))
if err != nil {
return nil, ResponseMetadata{}, err
}
upstreamURL, err := p.provider.URLResolver().Resolve(meta)
if err != nil {
return nil, ResponseMetadata{}, err
}
upstreamReq, err := http.NewRequestWithContext(ctx, req.Method, upstreamURL.String(), bytes.NewReader(body))
if err != nil {
return nil, ResponseMetadata{}, err
}
for k, v := range req.Header {
upstreamReq.Header[k] = v
}
if err := p.provider.RequestEnricher().Enrich(upstreamReq, meta, body); err != nil {
return nil, ResponseMetadata{}, err
}
ctxValue := MetaContextValue{Meta: meta, RawBody: body}
upstreamReq = upstreamReq.WithContext(context.WithValue(upstreamReq.Context(), MetaContextKey{}, ctxValue))
chain := p.globalInterceptors
roundTrip := p.roundTrip
if len(chain) > 0 {
roundTrip = chain.Wrap(roundTrip)
}
resp, respMeta, rawRespBody, err := roundTrip(upstreamReq)
if err != nil {
return nil, respMeta, err
}
// Re-attach the raw response body so the caller can read it
resp.Body = io.NopCloser(bytes.NewReader(rawRespBody))
return resp, respMeta, nil
}
func (p *Proxy) roundTrip(req *http.Request) (*http.Response, ResponseMetadata, []byte, error) {
resp, err := p.client.Do(req)
if err != nil {
return nil, ResponseMetadata{}, nil, err
}
respMeta, rawBody, err := p.provider.ResponseExtractor().Extract(resp)
if err != nil {
return nil, ResponseMetadata{}, nil, err
}
return resp, respMeta, rawBody, nil
}