feat: aibridge BYOK (#23013)

### Changes

  **coder/coder:**

- `coderd/aibridge/aibridge.go` — Added `HeaderCoderBYOKToken` constant,
`IsBYOK()` helper, and updated `ExtractAuthToken` to check the BYOK
header first.
- `enterprise/aibridged/http.go` — BYOK-aware header stripping: in BYOK
mode only the BYOK header is stripped (user's LLM credentials
preserved); in centralized mode all auth headers are stripped.
  
 <hr/>
 
**NOTE**: `X-Coder-Token` was removed! As of now `ExtractAuthToken`
retrieves token either from `X-Coder-AI-Governance-BYOK-Token` or from
`Authorization`/`X-Api-Key`.

---------

Co-authored-by: Susana Ferreira <susana@coder.com>
Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
Yevhenii Shcherbina
2026-03-25 14:17:56 -04:00
committed by GitHub
co-authored by Susana Ferreira Danny Kopping
parent 8576d1a9e9
commit a86b8ab6f8
7 changed files with 309 additions and 88 deletions
+20 -10
View File
@@ -6,18 +6,28 @@ import (
"strings"
)
// HeaderCoderAuth is an internal header used to pass the Coder token
// from AI Proxy to AI Bridge for authentication. This header is stripped
// by AI Bridge before forwarding requests to upstream providers.
const HeaderCoderAuth = "X-Coder-Token"
// HeaderCoderToken is a header set by clients opting into BYOK
// (Bring Your Own Key) mode. It carries the Coder token so
// that Authorization and X-Api-Key can carry the user's own LLM
// credentials. When present, AI Bridge forwards the user's LLM
// headers unchanged instead of injecting the centralized key.
//
// The AI Bridge proxy also sets this header automatically for clients
// that use per-user LLM credentials but cannot set custom headers.
const HeaderCoderToken = "X-Coder-AI-Governance-Token" //nolint:gosec // This is a header name, not a credential.
// ExtractAuthToken extracts an authorization token from HTTP headers.
// It checks X-Coder-Token first (set by AI Proxy), then falls back
// to Authorization header (Bearer token) and X-Api-Key header, which represent
// the different ways clients authenticate against AI providers.
// If none are present, an empty string is returned.
// IsBYOK reports whether the request is using BYOK mode, determined
// by the presence of the X-Coder-AI-Governance-Token header.
func IsBYOK(header http.Header) bool {
return strings.TrimSpace(header.Get(HeaderCoderToken)) != ""
}
// ExtractAuthToken extracts a token from HTTP headers.
// It checks the BYOK header first (set by clients opting into BYOK),
// then falls back to Authorization: Bearer and X-Api-Key for direct
// centralized mode. If none are present, an empty string is returned.
func ExtractAuthToken(header http.Header) string {
if token := strings.TrimSpace(header.Get(HeaderCoderAuth)); token != "" {
if token := strings.TrimSpace(header.Get(HeaderCoderToken)); token != "" {
return token
}
if auth := strings.TrimSpace(header.Get("Authorization")); auth != "" {
+112 -53
View File
@@ -174,44 +174,107 @@ func TestServeHTTP_FailureModes(t *testing.T) {
}
}
func TestServeHTTP_CoderTokenRemoved(t *testing.T) {
func TestServeHTTP_StripCoderToken(t *testing.T) {
t.Parallel()
mockHandler := &mockHandler{}
cases := []struct {
name string
reqHeaders map[string]string
expectPresent map[string]string // header → expected value
expectAbsent []string // headers that must be gone
}{
{
// Centralized: the client sets Authorization and X-Api-Key,
// but does not include HeaderCoderToken.
// All auth headers are stripped.
name: "centralized",
reqHeaders: map[string]string{
"Authorization": "Bearer coder-token",
"X-Api-Key": "sk-ant-api03-user-key",
},
expectAbsent: []string{
"Authorization",
"X-Api-Key",
agplaibridge.HeaderCoderToken,
},
},
{
// BYOK with access token: Coder token in BYOK header,
// user's access token in Authorization. Only the
// BYOK header is stripped.
name: "byok bearer token",
reqHeaders: map[string]string{
agplaibridge.HeaderCoderToken: "coder-token",
"Authorization": "Bearer sk-ant-oat01-user-oauth-token",
},
expectPresent: map[string]string{
"Authorization": "Bearer sk-ant-oat01-user-oauth-token",
},
expectAbsent: []string{
agplaibridge.HeaderCoderToken,
},
},
{
// BYOK with personal API key: Coder token in BYOK header,
// user's API key in X-Api-Key. Only the BYOK header is
// stripped.
name: "byok api key",
reqHeaders: map[string]string{
agplaibridge.HeaderCoderToken: "coder-token",
"X-Api-Key": "sk-ant-api03-user-key",
},
expectPresent: map[string]string{
"X-Api-Key": "sk-ant-api03-user-key",
},
expectAbsent: []string{
agplaibridge.HeaderCoderToken,
},
},
}
srv, client, pool := newTestServer(t)
conn := &mockDRPCConn{}
client.EXPECT().DRPCConn().AnyTimes().Return(conn)
client.EXPECT().IsAuthorized(gomock.Any(), gomock.Any()).AnyTimes().Return(&proto.IsAuthorizedResponse{OwnerId: uuid.NewString()}, nil)
pool.EXPECT().Acquire(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(mockHandler, nil)
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
httpSrv := httptest.NewServer(srv)
t.Cleanup(httpSrv.Close)
mockH := &mockHandler{}
ctx := testutil.Context(t, testutil.WaitShort)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, httpSrv.URL+"/openai/v1/chat/completions", nil)
require.NoError(t, err)
srv, client, pool := newTestServer(t)
conn := &mockDRPCConn{}
client.EXPECT().DRPCConn().AnyTimes().Return(conn)
client.EXPECT().IsAuthorized(gomock.Any(), gomock.Any()).AnyTimes().Return(&proto.IsAuthorizedResponse{OwnerId: uuid.NewString()}, nil)
pool.EXPECT().Acquire(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(mockH, nil)
// X-Coder-Token is used for authentication and should be stripped.
// Other authorization headers should be preserved.
req.Header.Set(agplaibridge.HeaderCoderAuth, "coder-token")
req.Header.Set("Authorization", "Bearer some-token")
req.Header.Set("X-Api-Key", "some-api-key")
httpSrv := httptest.NewServer(srv)
t.Cleanup(httpSrv.Close)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
ctx := testutil.Context(t, testutil.WaitShort)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, httpSrv.URL+"/openai/v1/chat/completions", nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
for k, v := range tc.reqHeaders {
req.Header.Set(k, v)
}
// Verify X-Coder-Token was removed before forwarding to handler.
require.NotNil(t, mockHandler.headersReceived)
require.Empty(t, mockHandler.headersReceived.Get(agplaibridge.HeaderCoderAuth),
"X-Coder-Token should be removed before forwarding to handler")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Verify other headers were preserved.
require.Equal(t, "Bearer some-token", mockHandler.headersReceived.Get("Authorization"))
require.Equal(t, "some-api-key", mockHandler.headersReceived.Get("X-Api-Key"))
require.Equal(t, http.StatusOK, resp.StatusCode)
require.NotNil(t, mockH.headersReceived)
for header, expected := range tc.expectPresent {
require.Equal(t, expected, mockH.headersReceived.Get(header),
"header %q should be preserved with value %q", header, expected)
}
for _, header := range tc.expectAbsent {
require.Empty(t, mockH.headersReceived.Get(header),
"header %q should be stripped", header)
}
// HeaderCoderToken should always be stripped
require.Empty(t, mockH.headersReceived.Get(agplaibridge.HeaderCoderToken),
"header %q should be stripped", agplaibridge.HeaderCoderToken)
})
}
}
func TestExtractAuthToken(t *testing.T) {
@@ -225,31 +288,6 @@ func TestExtractAuthToken(t *testing.T) {
{
name: "none",
},
{
name: "x-coder-token/empty",
headers: map[string]string{agplaibridge.HeaderCoderAuth: ""},
},
{
name: "x-coder-token/ok",
headers: map[string]string{agplaibridge.HeaderCoderAuth: "coder-token"},
expectedKey: "coder-token",
},
{
name: "x-coder-token/priority over authorization",
headers: map[string]string{
agplaibridge.HeaderCoderAuth: "coder-token",
"Authorization": "Bearer other-token",
},
expectedKey: "coder-token",
},
{
name: "x-coder-token/priority over x-api-key",
headers: map[string]string{
agplaibridge.HeaderCoderAuth: "coder-token",
"X-Api-Key": "api-key",
},
expectedKey: "coder-token",
},
{
name: "authorization/invalid",
headers: map[string]string{"authorization": "invalid"},
@@ -285,6 +323,27 @@ func TestExtractAuthToken(t *testing.T) {
headers: map[string]string{"X-Api-Key": "key"},
expectedKey: "key",
},
// BYOK: X-Coder-AI-Governance-Token carries the Coder
// token and has the highest priority.
{
name: "byok/empty",
headers: map[string]string{agplaibridge.HeaderCoderToken: ""},
},
{
name: "byok/ok",
headers: map[string]string{agplaibridge.HeaderCoderToken: "coder-token"},
expectedKey: "coder-token",
},
{
name: "byok/priority over all",
headers: map[string]string{
agplaibridge.HeaderCoderToken: "coder-token",
"Authorization": "Bearer oauth-token",
"X-Api-Key": "api-key",
},
expectedKey: "coder-token",
},
}
for _, tc := range cases {
+24 -3
View File
@@ -37,6 +37,12 @@ func (s *Server) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
logger := s.logger.With(slog.F("path", r.URL.Path))
byok := agplaibridge.IsBYOK(r.Header)
authMode := "centralized"
if byok {
authMode = "byok"
}
key := strings.TrimSpace(agplaibridge.ExtractAuthToken(r.Header))
if key == "" {
logger.Warn(ctx, "no auth key provided")
@@ -44,8 +50,23 @@ func (s *Server) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
return
}
// Remove the Coder token header so it's not forwarded to upstream providers.
r.Header.Del(agplaibridge.HeaderCoderAuth)
// Strip every header that may carry the Coder token so it is
// never forwarded to upstream providers. After stripping, the
// aibridge library can treat the request as a normal LLM API call
// with no Coder-specific information.
if byok {
// In BYOK mode the token is in X-Coder-AI-Governance-Token;
// Authorization and X-Api-Key carry the user's own LLM credentials
// and must be preserved.
r.Header.Del(agplaibridge.HeaderCoderToken)
} else {
// In centralized mode the token may be in Authorization (the
// documented path) or X-Api-Key (legacy clients that set
// ANTHROPIC_API_KEY to their Coder token). Both are
// stripped.
r.Header.Del("Authorization")
r.Header.Del("X-Api-Key")
}
client, err := s.Client()
if err != nil {
@@ -56,7 +77,7 @@ func (s *Server) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
resp, err := client.IsAuthorized(ctx, &proto.IsAuthorizedRequest{Key: key})
if err != nil {
logger.Warn(ctx, "key authorization check failed", slog.Error(err))
logger.Warn(ctx, "key authorization check failed", slog.Error(err), slog.F("auth_mode", authMode))
http.Error(rw, ErrUnauthorized.Error(), http.StatusForbidden)
return
}
+37 -6
View File
@@ -714,6 +714,17 @@ func extractCoderTokenFromProxyAuth(proxyAuth string) string {
return credentials[1]
}
// extractCoderTokenFromBearerAuth extracts the bearer token from an
// Authorization header. Returns empty string if the header is not a
// valid "Bearer <token>" value.
func extractCoderTokenFromBearerAuth(auth string) string {
parts := strings.Fields(auth)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return ""
}
return parts[1]
}
// newProxyAuthRequiredResponse creates a 407 Proxy Authentication Required
// response with the appropriate challenge header. This is used both during
// CONNECT handling and for decrypted requests missing authentication.
@@ -866,8 +877,10 @@ func (s *Server) checkBlockedIPAndDial(ctx context.Context, network, addr string
}
// handleRequest intercepts HTTP requests after MITM decryption.
// - Requests to known AI providers are rewritten to aibridged, with the Coder token
// (from ctx.UserData, set during CONNECT) set in the X-Coder-Token header.
// - Requests to known AI providers are rewritten to point at aibridged.
// In centralized mode the Coder token is already in the
// Authorization header. For BYOK clients that cannot set custom
// headers, the proxy injects the BYOK header.
// - Unknown hosts are passed through to the original upstream.
func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
originalPath := req.URL.Path
@@ -945,10 +958,7 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
req.URL = aiBridgeParsedURL
req.Host = aiBridgeParsedURL.Host
// Set X-Coder-Token header for aibridged authentication.
// Using a separate header preserves the original request headers,
// which are forwarded to upstream providers.
req.Header.Set(agplaibridge.HeaderCoderAuth, reqCtx.CoderToken)
injectBYOKHeaderIfNeeded(req.Header, reqCtx.CoderToken)
// Set custom header for cross-service log correlation.
// This allows correlating aibridgeproxyd logs with aibridged logs.
@@ -967,6 +977,27 @@ func (s *Server) handleRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.
return req, nil
}
// injectBYOKHeaderIfNeeded sets HeaderCoderToken when the
// Authorization header carries a bearer token that differs from the
// Coder token, indicating the client is using its own LLM
// credentials. Clients that can set custom headers
// do this themselves; this handles clients that cannot.
//
// In centralized mode, Authorization carries the Coder token
// itself, so aibridged discovers it via ExtractAuthToken
// without any extra header.
func injectBYOKHeaderIfNeeded(header http.Header, coderToken string) {
// Don’t overwrite the header if it’s already set.
if header.Get(agplaibridge.HeaderCoderToken) != "" {
return
}
bearer := extractCoderTokenFromBearerAuth(header.Get("Authorization"))
if bearer != "" && bearer != coderToken {
header.Set(agplaibridge.HeaderCoderToken, coderToken)
}
}
// handleResponse handles responses received from aibridged.
// This is only called for MITM'd requests (allowlisted domains routed through aibridged).
// Tunneled requests (non-allowlisted domains) bypass this handler entirely.
+113 -13
View File
@@ -1370,12 +1370,13 @@ func TestProxy_MITM(t *testing.T) {
metrics := aibridgeproxyd.NewMetrics(reg)
// Track what aibridged receives.
var receivedPath, receivedCoderToken, receivedRequestID string
var receivedPath, receivedAuthz, receivedBYOK, receivedRequestID string
// Create a mock aibridged server that captures requests.
aibridgedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedPath = r.URL.Path
receivedCoderToken = r.Header.Get(agplaibridge.HeaderCoderAuth)
receivedAuthz = r.Header.Get("Authorization")
receivedBYOK = r.Header.Get(agplaibridge.HeaderCoderToken)
receivedRequestID = r.Header.Get(aibridgeproxyd.HeaderAIBridgeRequestID)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello from aibridged"))
@@ -1427,11 +1428,14 @@ func TestProxy_MITM(t *testing.T) {
certPool = getProxyCertPool(t)
}
// Make a request through the proxy to the target URL.
client := newProxyClient(t, srv, makeProxyAuthHeader("test-token"), certPool, false)
// Simulate the primary proxy use case: the Coder
// token is in Proxy-Authorization, and the user's
// own LLM token is in Authorization.
client := newProxyClient(t, srv, makeProxyAuthHeader("coder-token"), certPool, false)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, targetURL, strings.NewReader(`{}`))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer user-llm-token")
resp, err := client.Do(req)
require.NoError(t, err)
@@ -1449,7 +1453,7 @@ func TestProxy_MITM(t *testing.T) {
// Verify request went to target server, not aibridged.
require.Equal(t, "hello from tunneled", string(body))
require.Empty(t, receivedPath, "aibridged should not receive tunneled requests")
require.Empty(t, receivedCoderToken, "tunneled requests are not authenticated by the proxy")
require.Empty(t, receivedAuthz, "tunneled requests should not reach aibridged")
require.Empty(t, receivedRequestID, "tunneled requests should not have request ID header")
// Verify metrics for tunneled requests.
@@ -1464,7 +1468,8 @@ func TestProxy_MITM(t *testing.T) {
// Verify the request was routed to aibridged correctly.
require.Equal(t, "hello from aibridged", string(body))
require.Equal(t, tt.expectedPath, receivedPath)
require.Equal(t, "test-token", receivedCoderToken, "MITM'd requests must include Coder token")
require.Equal(t, "Bearer user-llm-token", receivedAuthz, "user's LLM credentials must be forwarded")
require.Equal(t, "coder-token", receivedBYOK, "proxy must inject BYOK header with Coder token")
require.NotEmpty(t, receivedRequestID, "MITM'd requests must include request ID header")
_, err := uuid.Parse(receivedRequestID)
require.NoError(t, err, "request ID must be a valid UUID")
@@ -1482,6 +1487,95 @@ func TestProxy_MITM(t *testing.T) {
}
}
// TestProxy_MITM_BYOKInjection verifies that the proxy sets the BYOK header
// when Authorization carries a bearer token different from the Coder
// token. This handles clients that send per-user LLM credentials
// but cannot set custom headers.
func TestProxy_MITM_BYOKInjection(t *testing.T) {
t.Parallel()
coderToken := "coder-token"
tests := []struct {
name string
authzHeader string
byokHeader string // pre-set by client; empty means not set
expectBYOK bool
expectBYOKVal string
}{
{
// Centralized: Authorization carries the Coder token (same
// value as Proxy-Authorization). No BYOK header is set.
name: "Authorization matches Coder token",
authzHeader: "Bearer " + coderToken,
expectBYOK: false,
},
{
// BYOK: Authorization carries the user's token,
// which differs from the Coder token. The proxy injects
// the BYOK header.
name: "Authorization differs from Coder token",
authzHeader: "Bearer client-access-token",
expectBYOK: true,
expectBYOKVal: coderToken,
},
{
// Client already set the BYOK header (Claude Code, Codex).
// The proxy must not overwrite it.
name: "BYOK header already set by client — not overwritten",
authzHeader: "Bearer client-access-token",
byokHeader: "client-set-coder-token",
expectBYOK: true,
expectBYOKVal: "client-set-coder-token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var receivedBYOKHeader, receivedAuthz string
aibridgedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedAuthz = r.Header.Get("Authorization")
receivedBYOKHeader = r.Header.Get(agplaibridge.HeaderCoderToken)
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(aibridgedServer.Close)
srv := newTestProxy(t,
withCoderAccessURL(aibridgedServer.URL),
withDomainAllowlist(aibridgeproxyd.HostCopilot),
withAIBridgeProviderFromHost(nil),
)
certPool := getProxyCertPool(t)
client := newProxyClient(t, srv, makeProxyAuthHeader(coderToken), certPool, false)
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://"+aibridgeproxyd.HostCopilot+"/chat/completions", strings.NewReader(`{}`))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", tt.authzHeader)
if tt.byokHeader != "" {
req.Header.Set(agplaibridge.HeaderCoderToken, tt.byokHeader)
}
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, tt.authzHeader, receivedAuthz, "Authorization must be forwarded to aibridged")
if tt.expectBYOK {
require.Equal(t, tt.expectBYOKVal, receivedBYOKHeader, "BYOK header must be set when Authorization differs from Coder token")
} else {
require.Empty(t, receivedBYOKHeader, "BYOK header must not be set")
}
})
}
}
// TestListenerTLS verifies that the proxy works correctly when its listener is wrapped in TLS.
// It tests both tunneled and MITM'd requests through an HTTPS proxy listener.
func TestListenerTLS(t *testing.T) {
@@ -1785,7 +1879,8 @@ func TestUpstreamProxy(t *testing.T) {
finalDestinationBody string
aibridgeReceived bool
aibridgePath string
aibridgeCoderToken string
aibridgeAuthz string
aibridgeBYOK string
aibridgeBody string
)
@@ -1882,7 +1977,8 @@ func TestUpstreamProxy(t *testing.T) {
aibridgeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
aibridgeReceived = true
aibridgePath = r.URL.Path
aibridgeCoderToken = r.Header.Get(agplaibridge.HeaderCoderAuth)
aibridgeAuthz = r.Header.Get("Authorization")
aibridgeBYOK = r.Header.Get(agplaibridge.HeaderCoderToken)
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -1937,7 +2033,8 @@ func TestUpstreamProxy(t *testing.T) {
certPool = getProxyCertPool(t)
}
// Create HTTP client configured to use aiproxy.
// Create HTTP client configured to use aiproxy. Coder token
// in Proxy-Authorization, user's LLM token in Authorization.
client := newProxyClient(t, srv, makeProxyAuthHeader("test-coder-token"), certPool, false)
// Make request through aiproxy.
@@ -1945,6 +2042,7 @@ func TestUpstreamProxy(t *testing.T) {
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, targetURL, strings.NewReader(requestBody))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer user-llm-token")
resp, err := client.Do(req)
require.NoError(t, err)
@@ -1966,8 +2064,8 @@ func TestUpstreamProxy(t *testing.T) {
"final destination should receive the exact request body")
require.False(t, aibridgeReceived,
"aibridge should NOT receive request for non-allowlisted domain")
require.Empty(t, aibridgeCoderToken,
"tunneled requests should not have Coder token")
require.Empty(t, aibridgeAuthz,
"tunneled requests should not reach aibridge")
} else {
require.False(t, upstreamProxyCONNECTReceived,
"upstream proxy should NOT receive CONNECT for allowlisted domain")
@@ -1975,8 +2073,10 @@ func TestUpstreamProxy(t *testing.T) {
"aibridge should receive the MITM'd request")
require.Equal(t, tt.expectedAIBridgePath, aibridgePath,
"aibridge should receive rewritten path")
require.Equal(t, "test-coder-token", aibridgeCoderToken,
"aibridge should receive Coder token header")
require.Equal(t, "Bearer user-llm-token", aibridgeAuthz,
"user's LLM credentials must be forwarded")
require.Equal(t, "test-coder-token", aibridgeBYOK,
"proxy must inject BYOK header with Coder token")
require.Equal(t, requestBody, aibridgeBody,
"aibridge should receive the exact request body")
require.False(t, finalDestinationReceived,
+1 -1
View File
@@ -483,7 +483,7 @@ require (
github.com/anthropics/anthropic-sdk-go v1.19.0
github.com/brianvoe/gofakeit/v7 v7.14.0
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
github.com/coder/aibridge v1.0.8-0.20260316151612-5c071a7db41b
github.com/coder/aibridge v1.0.8-0.20260324203533-dd8c239e5566
github.com/coder/aisdk-go v0.0.9
github.com/coder/boundary v0.8.4-0.20260304164748-566aeea939ab
github.com/coder/preview v1.0.8
+2 -2
View File
@@ -314,8 +314,8 @@ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 h1:tRIViZ5JRmzdOEo5wUWngaGEFBG8OaE1o2GIHN5ujJ8=
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225/go.mod h1:rNLVpYgEVeu1Zk29K64z6Od8RBP9DwqCu9OfCzh8MR4=
github.com/coder/aibridge v1.0.8-0.20260316151612-5c071a7db41b h1:O470JUI+D8cuCSsPVSI6JMUq1JlKDdsPtk7feDCaLqQ=
github.com/coder/aibridge v1.0.8-0.20260316151612-5c071a7db41b/go.mod h1:u6WvGLMQQbk3ByeOw+LBdVgDNc/v/ujAtUc6MfvzQb4=
github.com/coder/aibridge v1.0.8-0.20260324203533-dd8c239e5566 h1:DK+a7Q9bPpTyq7ePaz81Ihauyp1ilXNhF8MI+7rmZpA=
github.com/coder/aibridge v1.0.8-0.20260324203533-dd8c239e5566/go.mod h1:u6WvGLMQQbk3ByeOw+LBdVgDNc/v/ujAtUc6MfvzQb4=
github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo=
github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M=
github.com/coder/boundary v0.8.4-0.20260304164748-566aeea939ab h1:HrlxyTmMQpOHfSKzRU1vf5TxrmV6vL5OiWq+Dvn5qh0=