Skip to content

Commit 56e5993

Browse files
authored
Merge pull request #615 from dgageot/refactor-jwt
Make Docker Desktop JWT available through the env provider
2 parents e7897a2 + 9d2eae4 commit 56e5993

6 files changed

Lines changed: 132 additions & 146 deletions

File tree

pkg/desktop/login.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package desktop
22

33
import (
44
"context"
5-
"os"
65
)
76

87
type DockerHubInfo struct {
@@ -12,13 +11,6 @@ type DockerHubInfo struct {
1211
}
1312

1413
func GetToken(ctx context.Context) string {
15-
// Allow the user to override the token via an environment variable.
16-
// This is e.g. useful when talking to a gateway on staging.
17-
manualToken := os.Getenv("DOCKER_TOKEN")
18-
if manualToken != "" {
19-
return manualToken
20-
}
21-
2214
var token string
2315
_ = ClientBackend.Get(ctx, "/registry/token", &token)
2416
return token

pkg/environment/default.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ func NewDefaultProvider() Provider {
1515
providers = append(providers, keychainProvider)
1616
}
1717

18+
// Append Docker Desktop provider last
19+
providers = append(providers, NewDockerDesktopProvider())
20+
1821
return NewMultiProvider(providers...)
1922
}

pkg/environment/docker-desktop.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package environment
2+
3+
import (
4+
"context"
5+
6+
"github.com/docker/cagent/pkg/desktop"
7+
)
8+
9+
const DockerDesktopTokenEnv = "DOCKER_TOKEN"
10+
11+
type DockerDesktopProvider struct{}
12+
13+
func NewDockerDesktopProvider() *DockerDesktopProvider {
14+
return &DockerDesktopProvider{}
15+
}
16+
17+
func (p *DockerDesktopProvider) Get(ctx context.Context, name string) string {
18+
if name != DockerDesktopTokenEnv {
19+
return ""
20+
}
21+
22+
return desktop.GetToken(ctx)
23+
}

pkg/model/provider/anthropic/client.go

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313

1414
"github.com/docker/cagent/pkg/chat"
1515
latest "github.com/docker/cagent/pkg/config/v2"
16-
"github.com/docker/cagent/pkg/desktop"
1716
"github.com/docker/cagent/pkg/environment"
1817
"github.com/docker/cagent/pkg/model/provider/base"
1918
"github.com/docker/cagent/pkg/model/provider/options"
@@ -24,11 +23,7 @@ import (
2423
// It holds the anthropic client and model config
2524
type Client struct {
2625
base.Config
27-
client anthropic.Client
28-
// When using the Docker AI Gateway, tokens are short-lived. We rebuild
29-
// the client per request when in gateway mode.
30-
useGateway bool
31-
gatewayBaseURL string
26+
clientFn func(context.Context) (anthropic.Client, error)
3227
}
3328

3429
// interleavedThinkingEnabled returns false unless explicitly enabled via
@@ -76,40 +71,47 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
7671
opt(&globalOptions)
7772
}
7873

79-
var requestOptions []option.RequestOption
80-
useGateway := false
81-
gatewayBaseURL := ""
74+
var clientFn func(context.Context) (anthropic.Client, error)
8275
if gateway := globalOptions.Gateway(); gateway == "" {
8376
authToken := env.Get(ctx, "ANTHROPIC_API_KEY")
8477
if authToken == "" {
8578
return nil, errors.New("ANTHROPIC_API_KEY environment variable is required")
8679
}
8780

8881
slog.Debug("Anthropic API key found, creating client")
89-
requestOptions = append(requestOptions,
82+
requestOptions := []option.RequestOption{
9083
option.WithAPIKey(authToken),
91-
)
84+
}
9285
if cfg.BaseURL != "" {
9386
requestOptions = append(requestOptions, option.WithBaseURL(cfg.BaseURL))
9487
}
88+
client := anthropic.NewClient(requestOptions...)
89+
clientFn = func(context.Context) (anthropic.Client, error) {
90+
return client, nil
91+
}
9592
} else {
96-
authToken := desktop.GetToken(ctx)
97-
if authToken == "" {
93+
// Fail fast if Docker Desktop's auth token isn't available
94+
if env.Get(ctx, environment.DockerDesktopTokenEnv) == "" {
9895
slog.Error("Anthropic client creation failed", "error", "failed to get Docker Desktop's authentication token")
9996
return nil, errors.New("sorry, you first need to sign in Docker Desktop to use the Docker AI Gateway")
10097
}
10198

102-
slog.Debug("Docker Desktop's authentication token found, creating client")
103-
requestOptions = append(requestOptions,
104-
option.WithAuthToken(authToken),
105-
option.WithAPIKey(authToken),
106-
option.WithBaseURL(gateway),
107-
)
108-
useGateway = true
109-
gatewayBaseURL = gateway
99+
// When using a Gateway, tokens are short-lived.
100+
clientFn = func(ctx context.Context) (anthropic.Client, error) {
101+
// Query a fresh auth token each time the client is used
102+
authToken := env.Get(ctx, environment.DockerDesktopTokenEnv)
103+
if authToken == "" {
104+
return anthropic.Client{}, errors.New("failed to get Docker Desktop token for Gateway")
105+
}
106+
107+
return anthropic.NewClient(
108+
option.WithAuthToken(authToken),
109+
option.WithAPIKey(authToken),
110+
option.WithBaseURL(gateway),
111+
), nil
112+
}
110113
}
111114

112-
client := anthropic.NewClient(requestOptions...)
113115
slog.Debug("Anthropic client created successfully", "model", cfg.Model)
114116

115117
if globalOptions.StructuredOutput != nil {
@@ -121,25 +123,10 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
121123
ModelConfig: cfg,
122124
ModelOptions: globalOptions,
123125
},
124-
client: client,
125-
useGateway: useGateway,
126-
gatewayBaseURL: gatewayBaseURL,
126+
clientFn: clientFn,
127127
}, nil
128128
}
129129

130-
// newGatewayClient builds a new Anthropic client using a fresh Docker Desktop token.
131-
func (c *Client) newGatewayClient(ctx context.Context) anthropic.Client {
132-
authToken := desktop.GetToken(ctx)
133-
opts := []option.RequestOption{
134-
option.WithAuthToken(authToken),
135-
option.WithAPIKey(authToken),
136-
}
137-
if c.gatewayBaseURL != "" {
138-
opts = append(opts, option.WithBaseURL(c.gatewayBaseURL))
139-
}
140-
return anthropic.NewClient(opts...)
141-
}
142-
143130
// CreateChatCompletionStream creates a streaming chat completion request
144131
func (c *Client) CreateChatCompletionStream(
145132
ctx context.Context,
@@ -155,10 +142,11 @@ func (c *Client) CreateChatCompletionStream(
155142
if maxTokens == 0 {
156143
maxTokens = 8192 // Default output budget when not specified
157144
}
158-
// Build a fresh client per request when using the gateway
159-
client := c.client
160-
if c.useGateway {
161-
client = c.newGatewayClient(ctx)
145+
146+
client, err := c.clientFn(ctx)
147+
if err != nil {
148+
slog.Error("Failed to create Anthropic client", "error", err)
149+
return nil, err
162150
}
163151

164152
// Use Beta API with interleaved thinking only when enabled

pkg/model/provider/gemini/client.go

Lines changed: 47 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313

1414
"github.com/docker/cagent/pkg/chat"
1515
latest "github.com/docker/cagent/pkg/config/v2"
16-
"github.com/docker/cagent/pkg/desktop"
1716
"github.com/docker/cagent/pkg/environment"
1817
"github.com/docker/cagent/pkg/model/provider/base"
1918
"github.com/docker/cagent/pkg/model/provider/options"
@@ -24,11 +23,7 @@ import (
2423
// It implements the provider.Provider interface
2524
type Client struct {
2625
base.Config
27-
client *genai.Client
28-
// When using the Docker AI Gateway, tokens are short-lived. We rebuild
29-
// the client per request when in gateway mode.
30-
useGateway bool
31-
gatewayBaseURL string
26+
clientFn func(context.Context) (*genai.Client, error)
3227
}
3328

3429
// NewClient creates a new Gemini client from the provided configuration
@@ -46,66 +41,63 @@ func NewClient(ctx context.Context, cfg *latest.ModelConfig, env environment.Pro
4641
opt(&modelOptions)
4742
}
4843

49-
var apiKey string
50-
var httpOptions genai.HTTPOptions
51-
useGateway := false
52-
gatewayBaseURL := ""
44+
var clientFn func(context.Context) (*genai.Client, error)
5345
if gateway := modelOptions.Gateway(); gateway == "" {
54-
apiKey = env.Get(ctx, "GOOGLE_API_KEY")
46+
apiKey := env.Get(ctx, "GOOGLE_API_KEY")
5547
if apiKey == "" {
5648
return nil, errors.New("GOOGLE_API_KEY environment variable is required")
5749
}
50+
51+
client, err := genai.NewClient(ctx, &genai.ClientConfig{
52+
APIKey: apiKey,
53+
Backend: genai.BackendGeminiAPI,
54+
})
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
clientFn = func(context.Context) (*genai.Client, error) {
60+
return client, nil
61+
}
5862
} else {
59-
// genai client requires a non-empty API key
60-
apiKey = desktop.GetToken(ctx)
61-
if apiKey == "" {
63+
// Fail fast if Docker Desktop's auth token isn't available
64+
if env.Get(ctx, environment.DockerDesktopTokenEnv) == "" {
65+
slog.Error("Gemini client creation failed", "error", "failed to get Docker Desktop's authentication token")
6266
return nil, errors.New("sorry, you first need to sign in Docker Desktop to use the Docker AI Gateway")
6367
}
64-
httpOptions.BaseURL = gateway
65-
httpOptions.Headers = make(http.Header)
66-
httpOptions.Headers.Set("Authorization", "Bearer "+apiKey)
67-
useGateway = true
68-
gatewayBaseURL = gateway
69-
}
7068

71-
client, err := genai.NewClient(ctx, &genai.ClientConfig{
72-
APIKey: apiKey,
73-
Backend: genai.BackendGeminiAPI,
74-
HTTPOptions: httpOptions,
75-
})
76-
if err != nil {
77-
return nil, err
69+
// When using a Gateway, tokens are short-lived.
70+
clientFn = func(ctx context.Context) (*genai.Client, error) {
71+
// Query a fresh auth token each time the client is used
72+
authToken := env.Get(ctx, environment.DockerDesktopTokenEnv)
73+
if authToken == "" {
74+
return nil, errors.New("failed to get Docker Desktop token for Gateway")
75+
}
76+
77+
return genai.NewClient(ctx, &genai.ClientConfig{
78+
APIKey: authToken,
79+
Backend: genai.BackendGeminiAPI,
80+
HTTPOptions: genai.HTTPOptions{
81+
BaseURL: gateway,
82+
Headers: http.Header{
83+
"Authorization": []string{"Bearer " + authToken},
84+
},
85+
},
86+
})
87+
}
7888
}
7989

90+
slog.Debug("Gemini client created successfully", "model", cfg.Model)
91+
8092
return &Client{
8193
Config: base.Config{
8294
ModelConfig: cfg,
8395
ModelOptions: modelOptions,
8496
},
85-
client: client,
86-
useGateway: useGateway,
87-
gatewayBaseURL: gatewayBaseURL,
97+
clientFn: clientFn,
8898
}, nil
8999
}
90100

91-
// newGatewayClient builds a new Gemini client using a fresh Docker Desktop token.
92-
func (c *Client) newGatewayClient(ctx context.Context) (*genai.Client, error) {
93-
token := desktop.GetToken(ctx)
94-
if token == "" {
95-
return nil, errors.New("failed to get Docker Desktop token for gateway")
96-
}
97-
httpOptions := genai.HTTPOptions{
98-
BaseURL: c.gatewayBaseURL,
99-
Headers: make(http.Header),
100-
}
101-
httpOptions.Headers.Set("Authorization", "Bearer "+token)
102-
return genai.NewClient(ctx, &genai.ClientConfig{
103-
APIKey: token,
104-
Backend: genai.BackendGeminiAPI,
105-
HTTPOptions: httpOptions,
106-
})
107-
}
108-
109101
// convertMessagesToGemini converts chat.Messages into Gemini Contents
110102
func convertMessagesToGemini(messages []chat.Message) []*genai.Content {
111103
contents := make([]*genai.Content, 0, len(messages))
@@ -338,16 +330,13 @@ func (c *Client) CreateChatCompletionStream(
338330
slog.Debug("Message", "index", i, "role", content.Role)
339331
}
340332

341-
// Build a fresh client per request when using the gateway
342-
var iter func(func(*genai.GenerateContentResponse, error) bool)
343-
if c.useGateway {
344-
if gwClient, err := c.newGatewayClient(ctx); err == nil {
345-
iter = gwClient.Models.GenerateContentStream(ctx, c.ModelConfig.Model, contents, config)
346-
} else {
347-
iter = c.client.Models.GenerateContentStream(ctx, c.ModelConfig.Model, contents, config)
348-
}
349-
} else {
350-
iter = c.client.Models.GenerateContentStream(ctx, c.ModelConfig.Model, contents, config)
333+
client, err := c.clientFn(ctx)
334+
if err != nil {
335+
slog.Error("Failed to create Gemini client", "error", err)
336+
return nil, err
351337
}
338+
339+
// Build a fresh client per request when using the gateway
340+
iter := client.Models.GenerateContentStream(ctx, c.ModelConfig.Model, contents, config)
352341
return NewStreamAdapter(iter, c.ModelConfig.Model), nil
353342
}

0 commit comments

Comments
 (0)