mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
refactor: move aibridged out of enterprise to AGPL (#25570)
In order to allow Coder Agents to use AI Gateway in OSS, we need to rehome the `aibridged`\-related code into the AGPL path. The HTTP API is only registered under enterprise so will still require the AI Governance Add-on to be present in order to use it, whereas Coder Agents uses an in-memory pipe to the same handlers.
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
//go:build !slim
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/aibridge"
|
||||
"github.com/coder/coder/v2/aibridge/config"
|
||||
"github.com/coder/coder/v2/aibridge/keypool"
|
||||
"github.com/coder/coder/v2/coderd"
|
||||
"github.com/coder/coder/v2/coderd/aibridged"
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func newAIBridgeDaemon(coderAPI *coderd.API, providers []aibridge.Provider) (*aibridged.Server, error) {
|
||||
ctx := context.Background()
|
||||
coderAPI.Logger.Debug(ctx, "starting in-memory aibridge daemon")
|
||||
|
||||
logger := coderAPI.Logger.Named("aibridged")
|
||||
|
||||
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, logger.Named("pool"), metrics, tracer) // TODO: configurable size.
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create request pool: %w", err)
|
||||
}
|
||||
|
||||
// Create daemon.
|
||||
srv, err := aibridged.New(ctx, pool, func(dialCtx context.Context) (aibridged.DRPCClient, error) {
|
||||
return coderAPI.CreateInMemoryAIBridgeServer(dialCtx)
|
||||
}, logger, tracer)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("start in-memory aibridge daemon: %w", err)
|
||||
}
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// BuildProviders constructs the list of AI providers from config.
|
||||
// It merges legacy single-provider env vars and indexed provider configs:
|
||||
// 1. Legacy providers (from CODER_AI_GATEWAY_OPENAI_KEY, etc.) are added first.
|
||||
// If a legacy name conflicts with an indexed provider, startup fails with
|
||||
// a clear error asking the admin to remove one or the other.
|
||||
// 2. Indexed providers (from CODER_AI_GATEWAY_PROVIDER_<N>_*) are added next.
|
||||
func BuildProviders(cfg codersdk.AIBridgeConfig) ([]aibridge.Provider, error) {
|
||||
var cbConfig *config.CircuitBreaker
|
||||
if cfg.CircuitBreakerEnabled.Value() {
|
||||
cbConfig = &config.CircuitBreaker{
|
||||
FailureThreshold: uint32(cfg.CircuitBreakerFailureThreshold.Value()), //nolint:gosec // Validated by serpent.Validate in deployment options.
|
||||
Interval: cfg.CircuitBreakerInterval.Value(),
|
||||
Timeout: cfg.CircuitBreakerTimeout.Value(),
|
||||
MaxRequests: uint32(cfg.CircuitBreakerMaxRequests.Value()), //nolint:gosec // Validated by serpent.Validate in deployment options.
|
||||
}
|
||||
}
|
||||
|
||||
var providers []aibridge.Provider
|
||||
usedNames := make(map[string]struct{})
|
||||
|
||||
// Collect names from indexed providers so we can detect conflicts
|
||||
// with legacy providers.
|
||||
for _, p := range cfg.Providers {
|
||||
name := p.Name
|
||||
if name == "" {
|
||||
name = p.Type
|
||||
}
|
||||
usedNames[name] = struct{}{}
|
||||
}
|
||||
|
||||
// Add legacy OpenAI provider if configured.
|
||||
if cfg.LegacyOpenAI.Key.String() != "" {
|
||||
if _, conflict := usedNames[aibridge.ProviderOpenAI]; conflict {
|
||||
return nil, xerrors.Errorf("legacy CODER_AI_GATEWAY_OPENAI_KEY (or CODER_AIBRIDGE_OPENAI_KEY) conflicts with indexed provider named %q; remove one or the other", aibridge.ProviderOpenAI)
|
||||
}
|
||||
providers = append(providers, aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{
|
||||
Name: aibridge.ProviderOpenAI,
|
||||
BaseURL: cfg.LegacyOpenAI.BaseURL.String(),
|
||||
Key: cfg.LegacyOpenAI.Key.String(),
|
||||
CircuitBreaker: cbConfig,
|
||||
SendActorHeaders: cfg.SendActorHeaders.Value(),
|
||||
}))
|
||||
usedNames[aibridge.ProviderOpenAI] = struct{}{}
|
||||
}
|
||||
|
||||
// Add legacy Anthropic provider if configured. Bedrock credentials
|
||||
// alone are sufficient, an Anthropic API key is not required when
|
||||
// using AWS Bedrock.
|
||||
if cfg.LegacyAnthropic.Key.String() != "" || getBedrockConfig(cfg.LegacyBedrock) != nil {
|
||||
if _, conflict := usedNames[aibridge.ProviderAnthropic]; conflict {
|
||||
return nil, xerrors.Errorf("legacy CODER_AI_GATEWAY_ANTHROPIC_KEY (or CODER_AIBRIDGE_ANTHROPIC_KEY) conflicts with indexed provider named %q; remove one or the other", aibridge.ProviderAnthropic)
|
||||
}
|
||||
var pool *keypool.Pool
|
||||
if key := cfg.LegacyAnthropic.Key.String(); key != "" {
|
||||
var err error
|
||||
pool, err = keypool.New([]string{key}, quartz.NewReal())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create legacy anthropic key pool: %w", err)
|
||||
}
|
||||
}
|
||||
providers = append(providers, aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{
|
||||
Name: aibridge.ProviderAnthropic,
|
||||
BaseURL: cfg.LegacyAnthropic.BaseURL.String(),
|
||||
KeyPool: pool,
|
||||
CircuitBreaker: cbConfig,
|
||||
SendActorHeaders: cfg.SendActorHeaders.Value(),
|
||||
}, getBedrockConfig(cfg.LegacyBedrock)))
|
||||
usedNames[aibridge.ProviderAnthropic] = struct{}{}
|
||||
}
|
||||
|
||||
// Add indexed providers.
|
||||
for _, p := range cfg.Providers {
|
||||
name := p.Name
|
||||
if name == "" {
|
||||
name = p.Type
|
||||
}
|
||||
switch p.Type {
|
||||
case aibridge.ProviderOpenAI:
|
||||
var pool *keypool.Pool
|
||||
if len(p.Keys) > 0 {
|
||||
var err error
|
||||
pool, err = keypool.New(p.Keys, quartz.NewReal())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create openai key pool for provider %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
providers = append(providers, aibridge.NewOpenAIProvider(aibridge.OpenAIConfig{
|
||||
Name: name,
|
||||
BaseURL: p.BaseURL,
|
||||
KeyPool: pool,
|
||||
APIDumpDir: p.DumpDir,
|
||||
CircuitBreaker: cbConfig,
|
||||
SendActorHeaders: cfg.SendActorHeaders.Value(),
|
||||
}))
|
||||
case aibridge.ProviderAnthropic:
|
||||
var pool *keypool.Pool
|
||||
if len(p.Keys) > 0 {
|
||||
var err error
|
||||
pool, err = keypool.New(p.Keys, quartz.NewReal())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create anthropic key pool for provider %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
providers = append(providers, aibridge.NewAnthropicProvider(aibridge.AnthropicConfig{
|
||||
Name: name,
|
||||
BaseURL: p.BaseURL,
|
||||
KeyPool: pool,
|
||||
APIDumpDir: p.DumpDir,
|
||||
CircuitBreaker: cbConfig,
|
||||
SendActorHeaders: cfg.SendActorHeaders.Value(),
|
||||
}, bedrockConfigFromProvider(p)))
|
||||
case aibridge.ProviderCopilot:
|
||||
providers = append(providers, aibridge.NewCopilotProvider(aibridge.CopilotConfig{
|
||||
Name: name,
|
||||
BaseURL: p.BaseURL,
|
||||
APIDumpDir: p.DumpDir,
|
||||
CircuitBreaker: cbConfig,
|
||||
}))
|
||||
default:
|
||||
return nil, xerrors.Errorf("unknown provider type %q for provider %q", p.Type, name)
|
||||
}
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// bedrockConfigFromProvider converts Bedrock fields from an indexed
|
||||
// AIProviderConfig into an aibridge AWSBedrockConfig.
|
||||
// Returns nil if no Bedrock fields are set.
|
||||
func bedrockConfigFromProvider(p codersdk.AIProviderConfig) *aibridge.AWSBedrockConfig {
|
||||
// Currently, only the first key pair is used, if any.
|
||||
// TODO(ssncferreira): pass a keypool.Pool instead.
|
||||
var accessKey, accessKeySecret string
|
||||
if len(p.BedrockAccessKeys) > 0 {
|
||||
accessKey = p.BedrockAccessKeys[0]
|
||||
}
|
||||
if len(p.BedrockAccessKeySecrets) > 0 {
|
||||
accessKeySecret = p.BedrockAccessKeySecrets[0]
|
||||
}
|
||||
settings := codersdk.NewAIProviderBedrockSettings(
|
||||
p.BedrockRegion, accessKey, accessKeySecret,
|
||||
p.BedrockModel, p.BedrockSmallFastModel,
|
||||
)
|
||||
if !codersdk.IsBedrockConfigured(p.BedrockBaseURL, settings) {
|
||||
return nil
|
||||
}
|
||||
return &aibridge.AWSBedrockConfig{
|
||||
BaseURL: p.BedrockBaseURL,
|
||||
Region: p.BedrockRegion,
|
||||
AccessKey: accessKey,
|
||||
AccessKeySecret: accessKeySecret,
|
||||
Model: p.BedrockModel,
|
||||
SmallFastModel: p.BedrockSmallFastModel,
|
||||
}
|
||||
}
|
||||
|
||||
func getBedrockConfig(cfg codersdk.AIBridgeBedrockConfig) *aibridge.AWSBedrockConfig {
|
||||
// codersdk.IsBedrockConfigured decides what counts as Bedrock; when
|
||||
// it returns false, the AWS SDK default credential chain (env vars,
|
||||
// shared config, IAM roles, etc.) is left to resolve credentials.
|
||||
settings := codersdk.NewAIProviderBedrockSettings(
|
||||
cfg.Region.String(),
|
||||
cfg.AccessKey.String(),
|
||||
cfg.AccessKeySecret.String(),
|
||||
cfg.Model.String(),
|
||||
cfg.SmallFastModel.String(),
|
||||
)
|
||||
if !codersdk.IsBedrockConfigured(cfg.BaseURL.String(), settings) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &aibridge.AWSBedrockConfig{
|
||||
BaseURL: cfg.BaseURL.String(),
|
||||
Region: cfg.Region.String(),
|
||||
AccessKey: cfg.AccessKey.String(),
|
||||
AccessKeySecret: cfg.AccessKeySecret.String(),
|
||||
Model: cfg.Model.String(),
|
||||
SmallFastModel: cfg.SmallFastModel.String(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
//go:build !slim
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/aibridge"
|
||||
agplaibridge "github.com/coder/coder/v2/coderd/aibridge"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func TestBuildProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("EmptyConfig", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
providers, err := BuildProviders(codersdk.AIBridgeConfig{})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, providers)
|
||||
})
|
||||
|
||||
t.Run("LegacyOnly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{}
|
||||
cfg.LegacyOpenAI.Key = serpent.String("sk-openai")
|
||||
cfg.LegacyAnthropic.Key = serpent.String("sk-anthropic")
|
||||
|
||||
providers, err := BuildProviders(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
names := providerNames(providers)
|
||||
assert.Contains(t, names, aibridge.ProviderOpenAI)
|
||||
assert.Contains(t, names, aibridge.ProviderAnthropic)
|
||||
assert.Len(t, names, 2)
|
||||
})
|
||||
|
||||
t.Run("IndexedOnly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{
|
||||
Type: aibridge.ProviderAnthropic,
|
||||
Name: "anthropic-zdr",
|
||||
Keys: []string{"sk-zdr"},
|
||||
DumpDir: "/tmp/anthropic-dump",
|
||||
},
|
||||
{
|
||||
Type: aibridge.ProviderOpenAI,
|
||||
Name: "openai-azure",
|
||||
Keys: []string{"sk-azure"},
|
||||
BaseURL: "https://azure.openai.com",
|
||||
DumpDir: "/tmp/openai-dump",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
providers, err := BuildProviders(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
names := providerNames(providers)
|
||||
assert.Equal(t, []string{"anthropic-zdr", "openai-azure"}, names)
|
||||
assert.Equal(t, "/tmp/anthropic-dump", providers[0].APIDumpDir())
|
||||
assert.Equal(t, "/tmp/openai-dump", providers[1].APIDumpDir())
|
||||
})
|
||||
|
||||
t.Run("LegacyOpenAIConflictsWithIndexed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-indexed"}},
|
||||
},
|
||||
}
|
||||
cfg.LegacyOpenAI.Key = serpent.String("sk-legacy")
|
||||
|
||||
_, err := BuildProviders(cfg)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "conflicts with indexed provider")
|
||||
})
|
||||
|
||||
t.Run("LegacyAnthropicConflictsWithIndexed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderAnthropic, Name: aibridge.ProviderAnthropic, Keys: []string{"sk-indexed"}},
|
||||
},
|
||||
}
|
||||
cfg.LegacyAnthropic.Key = serpent.String("sk-legacy")
|
||||
|
||||
_, err := BuildProviders(cfg)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "conflicts with indexed provider")
|
||||
})
|
||||
|
||||
t.Run("MixedLegacyAndIndexed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderAnthropic, Name: "anthropic-zdr", Keys: []string{"sk-zdr"}},
|
||||
},
|
||||
}
|
||||
cfg.LegacyOpenAI.Key = serpent.String("sk-openai")
|
||||
cfg.LegacyAnthropic.Key = serpent.String("sk-anthropic")
|
||||
|
||||
providers, err := BuildProviders(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
names := providerNames(providers)
|
||||
assert.Contains(t, names, aibridge.ProviderOpenAI)
|
||||
assert.Contains(t, names, aibridge.ProviderAnthropic)
|
||||
assert.Contains(t, names, "anthropic-zdr")
|
||||
})
|
||||
|
||||
t.Run("LegacyAnthropicWithBedrock", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{}
|
||||
cfg.LegacyAnthropic.Key = serpent.String("sk-anthropic")
|
||||
cfg.LegacyBedrock.Region = serpent.String("us-west-2")
|
||||
cfg.LegacyBedrock.AccessKey = serpent.String("AKID")
|
||||
cfg.LegacyBedrock.AccessKeySecret = serpent.String("secret")
|
||||
|
||||
providers, err := BuildProviders(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
names := providerNames(providers)
|
||||
assert.Equal(t, []string{aibridge.ProviderAnthropic}, names)
|
||||
})
|
||||
|
||||
t.Run("LegacyBedrockWithoutAnthropicKey", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Bedrock credentials alone should be enough to create an
|
||||
// Anthropic provider — no CODER_AIBRIDGE_ANTHROPIC_KEY needed.
|
||||
cfg := codersdk.AIBridgeConfig{}
|
||||
cfg.LegacyBedrock.Region = serpent.String("us-west-2")
|
||||
cfg.LegacyBedrock.AccessKey = serpent.String("AKID")
|
||||
cfg.LegacyBedrock.AccessKeySecret = serpent.String("secret")
|
||||
|
||||
providers, err := BuildProviders(cfg)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, providers, 1)
|
||||
|
||||
p := providers[0]
|
||||
assert.Equal(t, aibridge.ProviderAnthropic, p.Type())
|
||||
assert.Equal(t, aibridge.ProviderAnthropic, p.Name())
|
||||
})
|
||||
|
||||
t.Run("UnknownType", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: "gemini", Name: "gemini-pro"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := BuildProviders(cfg)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unknown provider type")
|
||||
})
|
||||
|
||||
t.Run("CopilotVariants", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Copilot providers can target any of the three GitHub
|
||||
// Copilot API hosts via an explicit BASE_URL.
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderCopilot, Name: aibridge.ProviderCopilot, DumpDir: "/tmp/copilot-dump"},
|
||||
{Type: aibridge.ProviderCopilot, Name: agplaibridge.ProviderCopilotBusiness, BaseURL: "https://" + agplaibridge.HostCopilotBusiness},
|
||||
{Type: aibridge.ProviderCopilot, Name: agplaibridge.ProviderCopilotEnterprise, BaseURL: "https://" + agplaibridge.HostCopilotEnterprise},
|
||||
},
|
||||
}
|
||||
|
||||
providers, err := BuildProviders(cfg)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, providers, 3)
|
||||
|
||||
assert.Equal(t, aibridge.ProviderCopilot, providers[0].Name())
|
||||
assert.Equal(t, "/tmp/copilot-dump", providers[0].APIDumpDir())
|
||||
assert.Equal(t, agplaibridge.ProviderCopilotBusiness, providers[1].Name())
|
||||
assert.Equal(t, "https://"+agplaibridge.HostCopilotBusiness, providers[1].BaseURL())
|
||||
assert.Equal(t, agplaibridge.ProviderCopilotEnterprise, providers[2].Name())
|
||||
assert.Equal(t, "https://"+agplaibridge.HostCopilotEnterprise, providers[2].BaseURL())
|
||||
})
|
||||
|
||||
t.Run("ChatGPTProvider", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// ChatGPT is an OpenAI-compatible provider with a custom
|
||||
// base URL. Admins configure it as an indexed openai provider.
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: agplaibridge.ProviderChatGPT, BaseURL: agplaibridge.BaseURLChatGPT},
|
||||
},
|
||||
}
|
||||
|
||||
providers, err := BuildProviders(cfg)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, providers, 1)
|
||||
|
||||
assert.Equal(t, agplaibridge.ProviderChatGPT, providers[0].Name())
|
||||
assert.Equal(t, agplaibridge.BaseURLChatGPT, providers[0].BaseURL())
|
||||
})
|
||||
}
|
||||
|
||||
func providerNames(providers []aibridge.Provider) []string {
|
||||
names := make([]string, len(providers))
|
||||
for i, p := range providers {
|
||||
names[i] = p.Name()
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -1026,6 +1026,29 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
return xerrors.Errorf("seed ai providers from env: %w", err)
|
||||
}
|
||||
|
||||
// In-memory aibridge daemon. Registered on coderd so chatd can
|
||||
// dispatch LLM requests via the in-process transport without
|
||||
// crossing the gated /api/v2/aibridge HTTP route. The HTTP route
|
||||
// itself is registered (and license-gated) only by enterprise/coderd;
|
||||
// in AGPL builds it does not exist at all. The daemon starts here
|
||||
// unconditionally when the bridge feature is enabled by config so
|
||||
// chatd can use it regardless of license entitlement.
|
||||
if vals.AI.BridgeConfig.Enabled.Value() {
|
||||
providers, err := BuildProviders(vals.AI.BridgeConfig)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("build AI providers: %w", err)
|
||||
}
|
||||
aibridgeDaemon, err := newAIBridgeDaemon(coderAPI, providers)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create aibridged: %w", err)
|
||||
}
|
||||
coderAPI.RegisterInMemoryAIBridgedHTTPHandler(aibridgeDaemon)
|
||||
// The handler is bound to coderAPI's lifecycle; Close() on the
|
||||
// daemon does not affect in-flight requests but is needed to
|
||||
// release pool/recorder resources at shutdown.
|
||||
defer aibridgeDaemon.Close()
|
||||
}
|
||||
|
||||
if vals.Prometheus.Enable {
|
||||
// Agent metrics require reference to the tailnet coordinator, so must be initiated after Coder API.
|
||||
closeAgentsFunc, err := prometheusmetrics.Agents(ctx, logger, options.PrometheusRegistry, coderAPI.Database, &coderAPI.TailnetCoordinator, coderAPI.DERPMap, coderAPI.Options.AgentInactiveDisconnectTimeout, 0)
|
||||
|
||||
Reference in New Issue
Block a user