mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat: synchronise provider changes with WatchAIProviders (#27091)
## Why PR #26797 was accidentally merged into the stale `graphite-base/26797` branch instead of `main` (Graphite picked the wrong base), so its changes never landed on `main`. This PR re-lands that work as a clean cherry-pick onto the current `main`. ## What Adds a `WatchAIProviders` streaming RPC to the `ProviderConfigurator` service so a running standalone AI Gateway refetches its provider set when the provider configuration changes. The server subscribes to `AIProvidersChangedChannel` (published by the provider CRUD endpoints) and forwards each event as a payload-free signal, plus one signal on subscribe; the gateway calls `GetAIProviders` on each signal to rebuild its pool. The aibridged API is bumped to v1.2. Env-seeded providers don't need a signal: seeding finishes before coderd serves the gateway connection, so the gateway's initial fetch already reflects the seeded set. ## For reviewers The change is split into two commits to make review easy: 1. **`feat: synchronise provider changes with WatchAIProviders`** is a faithful cherry-pick of #26797, identical to the originally reviewed PR. It is committed without pre-commit hooks because it does not build against current `main` on its own. 2. **`fix: resolve cherry-pick conflicts against main`** contains only the deltas needed to re-land on current `main`, and passes the full pre-commit suite: - `coderd/aibridged/proto/aibridged.pb.go` regenerated via the proto make target (the cherry-picked copy was generated against the older proto). - `enterprise/cli/aigatewaystart.go` import block unioned; `main` added `os` and `strings` while the PR added `sync`. - Three `aibridgedserver.NewServer` test call sites that landed on `main` after the original branch diverged now pass the new `pubsub` argument. Refs https://linear.app/codercom/issue/AIGOV-465 *This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.*
This commit is contained in:
+9
-7
@@ -61,7 +61,7 @@ func newAIBridgeDaemon(coderAPI *coderd.API, cfg codersdk.AIBridgeConfig, reg pr
|
||||
reg.MustRegister(keypool.NewStateCollector(pool.KeyPools))
|
||||
|
||||
// Create daemon. Construct it before subscribing so the reloader can use
|
||||
// srv.Client() to fetch providers over the in-memory RPC.
|
||||
// srv.ClientContext to fetch providers over the in-memory RPC.
|
||||
srv, err := aibridged.New(ctx, pool, func(dialCtx context.Context) (aibridged.DRPCClient, error) {
|
||||
return coderAPI.CreateInMemoryAIBridgeServer(dialCtx)
|
||||
}, logger, tracer)
|
||||
@@ -72,7 +72,7 @@ func newAIBridgeDaemon(coderAPI *coderd.API, cfg codersdk.AIBridgeConfig, reg pr
|
||||
// Subscribe to ai_providers change events so the pool tracks the database
|
||||
// without a restart, and perform the initial reload. The reload data path
|
||||
// is the in-memory RPC.
|
||||
reloader := NewPoolRPCReloader(pool, srv.Client, cfg, logger.Named("provider-loader"), metrics, providerMetrics)
|
||||
reloader := NewPoolRPCReloader(pool, srv.ClientContext, cfg, logger.Named("provider-loader"), metrics, providerMetrics)
|
||||
unsubscribe, err := aibridged.SubscribeProviderReload(ctx, coderAPI.Pubsub, reloader, logger.Named("provider-reload"))
|
||||
if err != nil {
|
||||
// Without the subscription the pool can never track provider changes,
|
||||
@@ -91,7 +91,7 @@ func newAIBridgeDaemon(coderAPI *coderd.API, cfg codersdk.AIBridgeConfig, reg pr
|
||||
// build, replace, and reload-metric accounting live in one place.
|
||||
type poolRPCReloader struct {
|
||||
pool *aibridged.CachedBridgePool
|
||||
client func() (aibridged.DRPCClient, error)
|
||||
client aibridged.ClientFuncWithContext
|
||||
cfg codersdk.AIBridgeConfig
|
||||
logger slog.Logger
|
||||
aibridgeMetrics *aibridge.Metrics
|
||||
@@ -100,10 +100,12 @@ type poolRPCReloader struct {
|
||||
|
||||
// NewPoolRPCReloader builds an [aibridged.ProviderReloader] that fetches the
|
||||
// provider set over the DRPC client returned by client and replaces pool's
|
||||
// providers, recording reload metrics against providerMetrics.
|
||||
// providers, recording reload metrics against providerMetrics. client receives
|
||||
// Reload's context, so a blocking acquisition unblocks when that context is
|
||||
// canceled.
|
||||
func NewPoolRPCReloader(
|
||||
pool *aibridged.CachedBridgePool,
|
||||
client func() (aibridged.DRPCClient, error),
|
||||
client aibridged.ClientFuncWithContext,
|
||||
cfg codersdk.AIBridgeConfig,
|
||||
logger slog.Logger,
|
||||
aibridgeMetrics *aibridge.Metrics,
|
||||
@@ -121,8 +123,8 @@ func NewPoolRPCReloader(
|
||||
|
||||
func (r *poolRPCReloader) Reload(ctx context.Context) error {
|
||||
r.providerMetrics.RecordReloadAttempt()
|
||||
// r.client() blocks until the daemon is connected to coderd.
|
||||
client, err := r.client()
|
||||
// r.client blocks until the daemon connects to coderd or ctx is canceled.
|
||||
client, err := r.client(ctx)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get ai-gateway client: %w", err)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func buildFromEnv(t *testing.T, cfg codersdk.AIBridgeConfig) ([]aibridge.Provide
|
||||
// (providers, outcomes) the embedded reloader would observe.
|
||||
func buildFromDB(ctx context.Context, t *testing.T, db database.Store, cfg codersdk.AIBridgeConfig, logger slog.Logger) ([]aibridge.Provider, []aibridged.ProviderOutcome, error) {
|
||||
t.Helper()
|
||||
srv, err := aibridgedserver.NewServer(ctx, db, logger, "/", cfg, nil, nil, agplaiseats.Noop{})
|
||||
srv, err := aibridgedserver.NewServer(ctx, db, nil, logger, "/", cfg, nil, nil, agplaiseats.Noop{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user