Skip to content

Commit 573650a

Browse files
committed
Set X-Cagent-Forward header and other telemetry headers
Signed-off-by: David Gageot <david.gageot@docker.com>
1 parent ef178b1 commit 573650a

4 files changed

Lines changed: 117 additions & 17 deletions

File tree

pkg/httpclient/client.go

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,101 @@ package httpclient
22

33
import (
44
"fmt"
5+
"maps"
56
"net/http"
67
"runtime"
78

89
"github.com/docker/cagent/pkg/version"
910
)
1011

12+
type HTTPOptions struct {
13+
Header http.Header
14+
}
15+
16+
type Opt func(*HTTPOptions)
17+
18+
func NewHTTPClient(opts ...Opt) *http.Client {
19+
httpOptions := HTTPOptions{
20+
Header: make(http.Header),
21+
}
22+
23+
for _, opt := range opts {
24+
opt(&httpOptions)
25+
}
26+
27+
// Enforce a consistent User-Agent header
28+
httpOptions.Header.Set("User-Agent", fmt.Sprintf("Cagent/%s (%s; %s)", version.Version, getNormalizedOS(), getNormalizedArchitecture()))
29+
30+
return &http.Client{
31+
Transport: &userAgentTransport{
32+
httpOptions: httpOptions,
33+
rt: http.DefaultTransport,
34+
},
35+
}
36+
}
37+
38+
func WithHeader(key, value string) Opt {
39+
return func(o *HTTPOptions) {
40+
o.Header.Set(key, value)
41+
}
42+
}
43+
44+
func WithProxiedBaseURL(value string) Opt {
45+
return func(o *HTTPOptions) {
46+
o.Header.Set("X-Cagent-Forward", value)
47+
48+
// Enforce consistent headers (Anthropic client sets similar header already)
49+
o.Header.Set("X-Cagent-Lang", "go")
50+
o.Header.Set("X-Cagent-OS", getNormalizedOS())
51+
o.Header.Set("X-Cagent-Arch", getNormalizedArchitecture())
52+
o.Header.Set("X-Cagent-Runtime", "cagent")
53+
o.Header.Set("X-Cagent-Runtime-Version", version.Version)
54+
}
55+
}
56+
1157
type userAgentTransport struct {
12-
agent string
13-
rt http.RoundTripper
58+
httpOptions HTTPOptions
59+
rt http.RoundTripper
1460
}
1561

1662
func (u *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
1763
r2 := req.Clone(req.Context())
18-
r2.Header.Set("User-Agent", u.agent)
64+
maps.Copy(r2.Header, u.httpOptions.Header)
1965
return u.rt.RoundTrip(r2)
2066
}
2167

22-
func NewHttpClient() *http.Client {
23-
return &http.Client{
24-
Transport: &userAgentTransport{
25-
agent: fmt.Sprintf("Cagent/%s (%s; %s)", version.Version, runtime.GOOS, runtime.GOARCH),
26-
rt: http.DefaultTransport,
27-
},
68+
func getNormalizedOS() string {
69+
switch runtime.GOOS {
70+
case "ios":
71+
return "iOS"
72+
case "android":
73+
return "Android"
74+
case "darwin":
75+
return "MacOS"
76+
case "window":
77+
return "Windows"
78+
case "freebsd":
79+
return "FreeBSD"
80+
case "openbsd":
81+
return "OpenBSD"
82+
case "linux":
83+
return "Linux"
84+
default:
85+
return fmt.Sprintf("Other:%s", runtime.GOOS)
86+
}
87+
}
88+
89+
func getNormalizedArchitecture() string {
90+
switch runtime.GOARCH {
91+
case "386":
92+
return "x32"
93+
case "amd64":
94+
return "x64"
95+
case "arm":
96+
return "arm"
97+
case "arm64":
98+
return "arm64"
99+
default:
100+
return fmt.Sprintf("other:%s", runtime.GOARCH)
28101
}
29102
}

pkg/model/provider/anthropic/client.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
8282
slog.Debug("Anthropic API key found, creating client")
8383
requestOptions := []option.RequestOption{
8484
option.WithAPIKey(authToken),
85-
option.WithHTTPClient(httpclient.NewHttpClient()),
85+
option.WithHTTPClient(httpclient.NewHTTPClient()),
8686
}
8787
if cfg.BaseURL != "" {
8888
requestOptions = append(requestOptions, option.WithBaseURL(cfg.BaseURL))
@@ -110,7 +110,9 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
110110
option.WithAuthToken(authToken),
111111
option.WithAPIKey(authToken),
112112
option.WithBaseURL(gateway),
113-
option.WithHTTPClient(httpclient.NewHttpClient()),
113+
option.WithHTTPClient(httpclient.NewHTTPClient(
114+
httpclient.WithProxiedBaseURL(defaultsTo(cfg.BaseURL, "https://api.anthropic.com/")),
115+
)),
114116
), nil
115117
}
116118
}
@@ -657,3 +659,10 @@ func countAnthropicTokens(
657659
}
658660
return result.InputTokens, nil
659661
}
662+
663+
func defaultsTo(value, defaultValue string) string {
664+
if value != "" {
665+
return value
666+
}
667+
return defaultValue
668+
}

pkg/model/provider/gemini/client.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
5252
client, err := genai.NewClient(ctx, &genai.ClientConfig{
5353
APIKey: apiKey,
5454
Backend: genai.BackendGeminiAPI,
55-
HTTPClient: httpclient.NewHttpClient(),
55+
HTTPClient: httpclient.NewHTTPClient(),
5656
HTTPOptions: genai.HTTPOptions{
5757
BaseURL: cfg.BaseURL,
5858
},
@@ -80,9 +80,11 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
8080
}
8181

8282
return genai.NewClient(ctx, &genai.ClientConfig{
83-
APIKey: authToken,
84-
Backend: genai.BackendGeminiAPI,
85-
HTTPClient: httpclient.NewHttpClient(),
83+
APIKey: authToken,
84+
Backend: genai.BackendGeminiAPI,
85+
HTTPClient: httpclient.NewHTTPClient(
86+
httpclient.WithProxiedBaseURL(defaultsTo(cfg.BaseURL, "https://generativelanguage.googleapis.com/")),
87+
),
8688
HTTPOptions: genai.HTTPOptions{
8789
BaseURL: gateway,
8890
Headers: http.Header{
@@ -346,3 +348,10 @@ func (c *Client) CreateChatCompletionStream(
346348
iter := client.Models.GenerateContentStream(ctx, c.ModelConfig.Model, contents, config)
347349
return NewStreamAdapter(iter, c.ModelConfig.Model), nil
348350
}
351+
352+
func defaultsTo(value, defaultValue string) string {
353+
if value != "" {
354+
return value
355+
}
356+
return defaultValue
357+
}

pkg/model/provider/openai/client.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
7272
openaiConfig.BaseURL = cfg.BaseURL
7373
}
7474

75-
openaiConfig.HTTPClient = httpclient.NewHttpClient()
75+
openaiConfig.HTTPClient = httpclient.NewHTTPClient()
7676

7777
// TODO: Move this logic to ProviderAliases as a config function
7878
if cfg.ProviderOpts != nil {
@@ -109,7 +109,9 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
109109

110110
openaiConfig := openai.DefaultConfig(authToken)
111111
openaiConfig.BaseURL = gateway + "/v1"
112-
openaiConfig.HTTPClient = httpclient.NewHttpClient()
112+
openaiConfig.HTTPClient = httpclient.NewHTTPClient(
113+
httpclient.WithProxiedBaseURL(defaultsTo(cfg.BaseURL, "https://api.openai.com/v1")),
114+
)
113115

114116
return openai.NewClientWithConfig(openaiConfig), nil
115117
}
@@ -393,3 +395,10 @@ type jsonSchema map[string]any
393395
func (j jsonSchema) MarshalJSON() ([]byte, error) {
394396
return json.Marshal(map[string]any(j))
395397
}
398+
399+
func defaultsTo(value, defaultValue string) string {
400+
if value != "" {
401+
return value
402+
}
403+
return defaultValue
404+
}

0 commit comments

Comments
 (0)