diff --git a/docs/ai-coder/ai-bridge/monitoring.md b/docs/ai-coder/ai-bridge/monitoring.md index b610a476da..10ca82ece7 100644 --- a/docs/ai-coder/ai-bridge/monitoring.md +++ b/docs/ai-coder/ai-bridge/monitoring.md @@ -60,3 +60,65 @@ needs. For configuration options and details, see [Data Retention](./setup.md#data-retention) in the AI Bridge setup guide. + +## Tracing + +AI Bridge supports tracing via [OpenTelemetry](https://opentelemetry.io/), +providing visibility into request processing, upstream API calls, and MCP server +interactions. + +### Enabling Tracing + +AI Bridge tracing is enabled when tracing is enabled for the Coder server. +To enable tracing set `CODER_TRACE_ENABLE` environment variable or +[--trace](https://coder.com/docs/reference/cli/server#--trace) CLI flag: + +```sh +export CODER_TRACE_ENABLE=true +``` + +```sh +coder server --trace +``` + +### What is Traced + +AI Bridge creates spans for the following operations: + +| Span Name | Description | +|---------------------------------------------|------------------------------------------------------| +| `CachedBridgePool.Acquire` | Acquiring a request bridge instance from the pool | +| `Intercept` | Top-level span for processing an intercepted request | +| `Intercept.CreateInterceptor` | Creating the request interceptor | +| `Intercept.ProcessRequest` | Processing the request through the bridge | +| `Intercept.ProcessRequest.Upstream` | Forwarding the request to the upstream AI provider | +| `Intercept.ProcessRequest.ToolCall` | Executing a tool call requested by the AI model | +| `Intercept.RecordInterception` | Recording creating interception record | +| `Intercept.RecordPromptUsage` | Recording prompt/message data | +| `Intercept.RecordTokenUsage` | Recording token consumption | +| `Intercept.RecordToolUsage` | Recording tool/function calls | +| `Intercept.RecordInterceptionEnded` | Recording the interception as completed | +| `ServerProxyManager.Init` | Initializing MCP server proxy connections | +| `StreamableHTTPServerProxy.Init` | Setting up HTTP-based MCP server proxies | +| `StreamableHTTPServerProxy.Init.fetchTools` | Fetching available tools from MCP servers | + +Example trace of an interception using Jaeger backend: + +![Trace of interception](../../images/aibridge/jaeger_interception_trace.png) + +### Capturing Logs in Traces + +> **Note:** Enabling log capture may generate a large volume of trace events. + +To include log messages as trace events, enable trace log capture +by setting `CODER_TRACE_LOGS` environment variable or using +[--trace-logs](https://coder.com/docs/reference/cli/server#--trace-logs) flag: + +```sh +export CODER_TRACE_ENABLE=true +export CODER_TRACE_LOGS=true +``` + +```sh +coder server --trace --trace-logs +``` diff --git a/docs/images/aibridge/jaeger_interception_trace.png b/docs/images/aibridge/jaeger_interception_trace.png new file mode 100644 index 0000000000..a7d13e32f8 Binary files /dev/null and b/docs/images/aibridge/jaeger_interception_trace.png differ diff --git a/enterprise/aibridged/aibridged.go b/enterprise/aibridged/aibridged.go index edbd353ccf..05ae57a37d 100644 --- a/enterprise/aibridged/aibridged.go +++ b/enterprise/aibridged/aibridged.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" "cdr.dev/slog" @@ -33,6 +34,7 @@ type Server struct { requestBridgePool Pooler logger slog.Logger + tracer trace.Tracer wg sync.WaitGroup // initConnectionCh will receive when the daemon connects to coderd for the @@ -48,7 +50,7 @@ type Server struct { shutdownOnce sync.Once } -func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger) (*Server, error) { +func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger, tracer trace.Tracer) (*Server, error) { if rpcDialer == nil { return nil, xerrors.Errorf("nil rpcDialer given") } @@ -56,6 +58,7 @@ func New(ctx context.Context, pool Pooler, rpcDialer Dialer, logger slog.Logger) ctx, cancel := context.WithCancel(ctx) daemon := &Server{ logger: logger, + tracer: tracer, clientDialer: rpcDialer, clientCh: make(chan DRPCClient), lifecycleCtx: ctx, @@ -143,7 +146,7 @@ func (s *Server) GetRequestHandler(ctx context.Context, req Request) (http.Handl return nil, xerrors.New("nil requestBridgePool") } - reqBridge, err := s.requestBridgePool.Acquire(ctx, req, s.Client, NewMCPProxyFactory(s.logger, s.Client)) + reqBridge, err := s.requestBridgePool.Acquire(ctx, req, s.Client, NewMCPProxyFactory(s.logger, s.tracer, s.Client)) if err != nil { return nil, xerrors.Errorf("acquire request bridge: %w", err) } diff --git a/enterprise/aibridged/aibridged_integration_test.go b/enterprise/aibridged/aibridged_integration_test.go index cb1a3ed8f1..3fcb217674 100644 --- a/enterprise/aibridged/aibridged_integration_test.go +++ b/enterprise/aibridged/aibridged_integration_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "slices" "testing" "time" @@ -13,8 +14,13 @@ import ( promtest "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" "github.com/coder/aibridge" + aibtracing "github.com/coder/aibridge/tracing" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" @@ -28,6 +34,8 @@ import ( "github.com/coder/coder/v2/testutil" ) +var testTracer = otel.Tracer("aibridged_test") + // TestIntegration is not an exhaustive test against the upstream AI providers' SDKs (see coder/aibridge for those). // This test validates that: // - intercepted requests can be authenticated/authorized @@ -35,11 +43,17 @@ import ( // - responses can be returned as expected // - interceptions are logged, as well as their related prompt, token, and tool calls // - MCP server configurations are returned as expected +// - tracing spans are properly recorded func TestIntegration(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) + sr := tracetest.NewSpanRecorder() + tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(sr)) + tracer := tp.Tracer(t.Name()) + defer func() { _ = tp.Shutdown(t.Context()) }() + // Create mock MCP server. var mcpTokenReceived string mockMCPServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -169,13 +183,13 @@ func TestIntegration(t *testing.T) { logger := testutil.Logger(t) providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})} - pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, nil, logger) + pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger, nil, tracer) require.NoError(t, err) // Given: aibridged is started. srv, err := aibridged.New(t.Context(), pool, func(ctx context.Context) (aibridged.DRPCClient, error) { return aiBridgeClient, nil - }, logger) + }, logger, tracer) require.NoError(t, err, "create new aibridged") t.Cleanup(func() { _ = srv.Shutdown(ctx) @@ -256,6 +270,44 @@ func TestIntegration(t *testing.T) { // Then: the MCP server was initialized. require.Contains(t, mcpTokenReceived, authLink.OAuthAccessToken, "mock MCP server not requested") + + // Then: verify tracing spans were recorded. + spans := sr.Ended() + require.NotEmpty(t, spans) + i := slices.IndexFunc(spans, func(s sdktrace.ReadOnlySpan) bool { return s.Name() == "CachedBridgePool.Acquire" }) + require.NotEqual(t, -1, i, "span named 'CachedBridgePool.Acquire' not found") + + expectAttrs := []attribute.KeyValue{ + attribute.String(aibtracing.InitiatorID, user.ID.String()), + attribute.String(aibtracing.APIKeyID, keyID), + } + require.Equal(t, spans[i].Attributes(), expectAttrs) + + // Check for aibridge spans. + spanNames := make(map[string]bool) + for _, span := range spans { + spanNames[span.Name()] = true + } + + expectedAibridgeSpans := []string{ + "CachedBridgePool.Acquire", + "ServerProxyManager.Init", + "StreamableHTTPServerProxy.Init", + "StreamableHTTPServerProxy.Init.fetchTools", + "Intercept", + "Intercept.CreateInterceptor", + "Intercept.RecordInterception", + "Intercept.ProcessRequest", + "Intercept.ProcessRequest.Upstream", + "Intercept.RecordPromptUsage", + "Intercept.RecordTokenUsage", + "Intercept.RecordToolUsage", + "Intercept.RecordInterceptionEnded", + } + + for _, expectedSpan := range expectedAibridgeSpans { + require.Contains(t, spanNames, expectedSpan) + } } // TestIntegrationWithMetrics validates that Prometheus metrics are correctly incremented @@ -324,13 +376,13 @@ func TestIntegrationWithMetrics(t *testing.T) { providers := []aibridge.Provider{aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: mockOpenAI.URL})} // Create pool with metrics. - pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, metrics, logger) + pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger, metrics, testTracer) require.NoError(t, err) // Given: aibridged is started. srv, err := aibridged.New(ctx, pool, func(ctx context.Context) (aibridged.DRPCClient, error) { return aiBridgeClient, nil - }, logger) + }, logger, testTracer) require.NoError(t, err, "create new aibridged") t.Cleanup(func() { _ = srv.Shutdown(ctx) diff --git a/enterprise/aibridged/aibridged_test.go b/enterprise/aibridged/aibridged_test.go index a66f3157a0..6a84fb3841 100644 --- a/enterprise/aibridged/aibridged_test.go +++ b/enterprise/aibridged/aibridged_test.go @@ -41,7 +41,7 @@ func newTestServer(t *testing.T) (*aibridged.Server, *mock.MockDRPCClient, *mock pool, func(ctx context.Context) (aibridged.DRPCClient, error) { return client, nil - }, logger) + }, logger, testTracer) require.NoError(t, err, "create new aibridged") t.Cleanup(func() { srv.Shutdown(context.Background()) @@ -290,7 +290,7 @@ func TestRouting(t *testing.T) { aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{BaseURL: openaiSrv.URL}), aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{BaseURL: antSrv.URL}, nil), } - pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, nil, logger) + pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger, nil, testTracer) require.NoError(t, err) conn := &mockDRPCConn{} client.EXPECT().DRPCConn().AnyTimes().Return(conn) @@ -309,7 +309,7 @@ func TestRouting(t *testing.T) { // Given: aibridged is started. srv, err := aibridged.New(t.Context(), pool, func(ctx context.Context) (aibridged.DRPCClient, error) { return client, nil - }, logger) + }, logger, testTracer) require.NoError(t, err, "create new aibridged") t.Cleanup(func() { _ = srv.Shutdown(testutil.Context(t, testutil.WaitShort)) diff --git a/enterprise/aibridged/mcp.go b/enterprise/aibridged/mcp.go index ab6d1d0031..23ca617b62 100644 --- a/enterprise/aibridged/mcp.go +++ b/enterprise/aibridged/mcp.go @@ -6,6 +6,7 @@ import ( "regexp" "time" + "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" "cdr.dev/slog" @@ -28,30 +29,32 @@ type MCPProxyBuilder interface { // The SessionKey from [Request] is used to authenticate against the Coder MCP server. // // NOTE: the [mcp.ServerProxier] instance may be proxying one or more MCP servers. - Build(ctx context.Context, req Request) (mcp.ServerProxier, error) + Build(ctx context.Context, req Request, tracer trace.Tracer) (mcp.ServerProxier, error) } var _ MCPProxyBuilder = &MCPProxyFactory{} type MCPProxyFactory struct { logger slog.Logger + tracer trace.Tracer clientFn ClientFunc } -func NewMCPProxyFactory(logger slog.Logger, clientFn ClientFunc) *MCPProxyFactory { +func NewMCPProxyFactory(logger slog.Logger, tracer trace.Tracer, clientFn ClientFunc) *MCPProxyFactory { return &MCPProxyFactory{ logger: logger, + tracer: tracer, clientFn: clientFn, } } -func (m *MCPProxyFactory) Build(ctx context.Context, req Request) (mcp.ServerProxier, error) { +func (m *MCPProxyFactory) Build(ctx context.Context, req Request, tracer trace.Tracer) (mcp.ServerProxier, error) { proxiers, err := m.retrieveMCPServerConfigs(ctx, req) if err != nil { return nil, xerrors.Errorf("resolve configs: %w", err) } - return mcp.NewServerProxyManager(proxiers), nil + return mcp.NewServerProxyManager(proxiers, tracer), nil } func (m *MCPProxyFactory) retrieveMCPServerConfigs(ctx context.Context, req Request) (map[string]mcp.ServerProxier, error) { @@ -173,7 +176,6 @@ func (m *MCPProxyFactory) newStreamableHTTPServerProxy(cfg *proto.MCPServerConfi // The proxy could then use its interface to retrieve a new access token and re-establish a connection. // For now though, the short TTL of this cache should mostly mask this problem. srv, err := mcp.NewStreamableHTTPServerProxy( - m.logger.Named(fmt.Sprintf("mcp-server-proxy-%s", cfg.GetId())), cfg.GetId(), cfg.GetUrl(), // See https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#token-requirements. @@ -182,6 +184,8 @@ func (m *MCPProxyFactory) newStreamableHTTPServerProxy(cfg *proto.MCPServerConfi }, allowlist, denylist, + m.logger.Named(fmt.Sprintf("mcp-server-proxy-%s", cfg.GetId())), + m.tracer, ) if err != nil { return nil, xerrors.Errorf("create streamable HTTP MCP server proxy: %w", err) diff --git a/enterprise/aibridged/mcp_internal_test.go b/enterprise/aibridged/mcp_internal_test.go index 37fb6fe2c2..5dc9bdd80b 100644 --- a/enterprise/aibridged/mcp_internal_test.go +++ b/enterprise/aibridged/mcp_internal_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" "github.com/coder/coder/v2/enterprise/aibridged/proto" "github.com/coder/coder/v2/testutil" @@ -42,7 +43,7 @@ func TestMCPRegex(t *testing.T) { t.Parallel() logger := testutil.Logger(t) - f := NewMCPProxyFactory(logger, nil) + f := NewMCPProxyFactory(logger, otel.Tracer("aibridged_test"), nil) _, err := f.newStreamableHTTPServerProxy(&proto.MCPServerConfig{ Id: "mock", diff --git a/enterprise/aibridged/pool.go b/enterprise/aibridged/pool.go index d043f533cd..1fe5b11fe5 100644 --- a/enterprise/aibridged/pool.go +++ b/enterprise/aibridged/pool.go @@ -7,13 +7,15 @@ import ( "time" "github.com/dgraph-io/ristretto/v2" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "golang.org/x/xerrors" "tailscale.com/util/singleflight" "cdr.dev/slog" - "github.com/coder/aibridge" "github.com/coder/aibridge/mcp" + "github.com/coder/aibridge/tracing" ) const ( @@ -52,12 +54,13 @@ type CachedBridgePool struct { singleflight *singleflight.Group[string, *aibridge.RequestBridge] metrics *aibridge.Metrics + tracer trace.Tracer shutDownOnce sync.Once shuttingDownCh chan struct{} } -func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, metrics *aibridge.Metrics, logger slog.Logger) (*CachedBridgePool, error) { +func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, logger slog.Logger, metrics *aibridge.Metrics, tracer trace.Tracer) (*CachedBridgePool, error) { cache, err := ristretto.NewCache(&ristretto.Config[string, *aibridge.RequestBridge]{ NumCounters: options.MaxItems * 10, // Docs suggest setting this 10x number of keys. MaxCost: options.MaxItems * cacheCost, // Up to n instances. @@ -85,13 +88,13 @@ func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, met return &CachedBridgePool{ cache: cache, providers: providers, - logger: logger, options: options, + metrics: metrics, + tracer: tracer, + logger: logger, singleflight: &singleflight.Group[string, *aibridge.RequestBridge]{}, - metrics: metrics, - shuttingDownCh: make(chan struct{}), }, nil } @@ -100,7 +103,15 @@ func NewCachedBridgePool(options PoolOptions, providers []aibridge.Provider, met // // Each returned [*aibridge.RequestBridge] is safe for concurrent use. // Each [*aibridge.RequestBridge] is stateful because it has MCP clients which maintain sessions to the configured MCP server. -func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn ClientFunc, mcpProxyFactory MCPProxyBuilder) (http.Handler, error) { +func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn ClientFunc, mcpProxyFactory MCPProxyBuilder) (_ http.Handler, outErr error) { + spanAttrs := []attribute.KeyValue{ + attribute.String(tracing.InitiatorID, req.InitiatorID.String()), + attribute.String(tracing.APIKeyID, req.APIKeyID), + } + ctx, span := p.tracer.Start(ctx, "CachedBridgePool.Acquire", trace.WithAttributes(spanAttrs...)) + defer tracing.EndSpanErr(span, &outErr) + ctx = tracing.WithRequestBridgeAttributesInContext(ctx, spanAttrs) + if err := ctx.Err(); err != nil { return nil, xerrors.Errorf("acquire: %w", err) } @@ -124,10 +135,12 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl // expire after the original TTL; we can extend the TTL on each Acquire() call. // For now, we need to let the instance expiry to keep the MCP connections fresh. + span.AddEvent("cache_hit") return bridge, nil } - recorder := aibridge.NewRecorder(p.logger.Named("recorder"), func() (aibridge.Recorder, error) { + span.AddEvent("cache_miss") + recorder := aibridge.NewRecorder(p.logger.Named("recorder"), p.tracer, func() (aibridge.Recorder, error) { client, err := clientFn() if err != nil { return nil, xerrors.Errorf("acquire client: %w", err) @@ -145,7 +158,7 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl err error ) - mcpServers, err = mcpProxyFactory.Build(ctx, req) + mcpServers, err = mcpProxyFactory.Build(ctx, req, p.tracer) if err != nil { p.logger.Warn(ctx, "failed to create MCP server proxiers", slog.Error(err)) // Don't fail here; MCP server injection can gracefully degrade. @@ -158,7 +171,7 @@ func (p *CachedBridgePool) Acquire(ctx context.Context, req Request, clientFn Cl } } - bridge, err := aibridge.NewRequestBridge(ctx, p.providers, recorder, mcpServers, p.metrics, p.logger) + bridge, err := aibridge.NewRequestBridge(ctx, p.providers, recorder, mcpServers, p.logger, p.metrics, p.tracer) if err != nil { return nil, xerrors.Errorf("create new request bridge: %w", err) } diff --git a/enterprise/aibridged/pool_test.go b/enterprise/aibridged/pool_test.go index d1b07d293d..5a22a12d0b 100644 --- a/enterprise/aibridged/pool_test.go +++ b/enterprise/aibridged/pool_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" "go.uber.org/mock/gomock" "cdr.dev/slog/sloggers/slogtest" @@ -30,7 +31,7 @@ func TestPool(t *testing.T) { mcpProxy := mcpmock.NewMockServerProxier(ctrl) opts := aibridged.PoolOptions{MaxItems: 1, TTL: time.Second} - pool, err := aibridged.NewCachedBridgePool(opts, nil, nil, logger) + pool, err := aibridged.NewCachedBridgePool(opts, nil, logger, nil, testTracer) require.NoError(t, err) t.Cleanup(func() { pool.Shutdown(context.Background()) }) @@ -120,6 +121,6 @@ func newMockMCPFactory(proxy *mcpmock.MockServerProxier) *mockMCPFactory { return &mockMCPFactory{proxy: proxy} } -func (m *mockMCPFactory) Build(ctx context.Context, req aibridged.Request) (mcp.ServerProxier, error) { +func (m *mockMCPFactory) Build(ctx context.Context, req aibridged.Request, tracer trace.Tracer) (mcp.ServerProxier, error) { return m.proxy, nil } diff --git a/enterprise/cli/aibridged.go b/enterprise/cli/aibridged.go index e955c4fcbe..986e8d59df 100644 --- a/enterprise/cli/aibridged.go +++ b/enterprise/cli/aibridged.go @@ -10,6 +10,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/coder/aibridge" + "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/enterprise/aibridged" "github.com/coder/coder/v2/enterprise/coderd" @@ -35,9 +36,10 @@ func newAIBridgeDaemon(coderAPI *coderd.API) (*aibridged.Server, error) { reg := prometheus.WrapRegistererWithPrefix("coder_aibridged_", coderAPI.PrometheusRegistry) metrics := aibridge.NewMetrics(reg) + tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName) // Create pool for reusable stateful [aibridge.RequestBridge] instances (one per user). - pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, metrics, logger.Named("pool")) // TODO: configurable size. + pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger.Named("pool"), metrics, tracer) // TODO: configurable size. if err != nil { return nil, xerrors.Errorf("create request pool: %w", err) } @@ -45,7 +47,7 @@ func newAIBridgeDaemon(coderAPI *coderd.API) (*aibridged.Server, error) { // Create daemon. srv, err := aibridged.New(ctx, pool, func(dialCtx context.Context) (aibridged.DRPCClient, error) { return coderAPI.CreateInMemoryAIBridgeServer(dialCtx) - }, logger) + }, logger, tracer) if err != nil { return nil, xerrors.Errorf("start in-memory aibridge daemon: %w", err) } diff --git a/go.mod b/go.mod index 17fb110747..9042d51ba4 100644 --- a/go.mod +++ b/go.mod @@ -476,7 +476,7 @@ require ( github.com/anthropics/anthropic-sdk-go v1.19.0 github.com/brianvoe/gofakeit/v7 v7.9.0 github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 - github.com/coder/aibridge v0.2.2 + github.com/coder/aibridge v0.3.0 github.com/coder/aisdk-go v0.0.9 github.com/coder/boundary v1.0.1-0.20250925154134-55a44f2a7945 github.com/coder/preview v1.0.4 diff --git a/go.sum b/go.sum index 6f57f5d803..dc28e09ac1 100644 --- a/go.sum +++ b/go.sum @@ -919,8 +919,8 @@ github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= 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 v0.2.2 h1:bTsz4nBD3znxkgrzzz7D5Ymx9nFUfvR500kevVN3xrA= -github.com/coder/aibridge v0.2.2/go.mod h1:2T0RSnIX1WTqFajzXsaNsoNe6mmNsNeCTxiHBWEsFnE= +github.com/coder/aibridge v0.3.0 h1:z5coky9A5uXOr+zjgmsynal8PVYBMmxE9u1vcIzs4t8= +github.com/coder/aibridge v0.3.0/go.mod h1:ENnl6VhU8Qot5OuVYqs7V4vXII11oKBWgWKrgIJbRAs= 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 v1.0.1-0.20250925154134-55a44f2a7945 h1:hDUf02kTX8EGR3+5B+v5KdYvORs4YNfDPci0zCs+pC0=