mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +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,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -109,14 +110,12 @@ func (r *RootCmd) aiGatewayStart() *serpent.Command {
|
||||
defer srv.Close()
|
||||
|
||||
// Fetch the initial provider set from coderd, retrying until
|
||||
// success.
|
||||
// TODO(AIGOV-465): the standalone gateway has no refresh trigger
|
||||
// yet, so this runs once on startup.
|
||||
clientFn := func() (aibridged.DRPCClient, error) {
|
||||
return srv.ClientContext(signalCtx)
|
||||
}
|
||||
// success. Subsequent changes are delivered by the watch loop
|
||||
// started below. The reloader's client acquisition honors the
|
||||
// context of each Reload call, so loadProviders is bounded by
|
||||
// signalCtx and the watch loop by watchCtx.
|
||||
providerLogger := logger.Named("aibridge.providers")
|
||||
reloader := agpl.NewPoolRPCReloader(pool, clientFn, vals.AI.BridgeConfig, providerLogger, metrics, providerMetrics)
|
||||
reloader := agpl.NewPoolRPCReloader(pool, srv.ClientContext, vals.AI.BridgeConfig, providerLogger, metrics, providerMetrics)
|
||||
if err := loadProviders(signalCtx, reloader, providerLogger, srv.Done()); err != nil {
|
||||
if signalCtx.Err() != nil {
|
||||
logger.Info(signalCtx, "shutting down standalone AI Gateway")
|
||||
@@ -127,6 +126,23 @@ func (r *RootCmd) aiGatewayStart() *serpent.Command {
|
||||
|
||||
mw := coderd.AIGatewayDataPlaneMiddleware(vals.AI.BridgeConfig)
|
||||
|
||||
// Watch coderd for provider changes and refresh the pool on each
|
||||
// signal.
|
||||
watchCtx, watchCancel := context.WithCancel(signalCtx)
|
||||
var watchWG sync.WaitGroup
|
||||
watchWG.Go(func() {
|
||||
// srv.ClientContext observes watchCtx, so watchCancel below
|
||||
// unblocks a pending client acquisition and drains this
|
||||
// goroutine without relying on srv.Close.
|
||||
if err := aibridged.WatchProviderReload(watchCtx, srv.ClientContext, reloader, providerLogger); err != nil && watchCtx.Err() == nil {
|
||||
providerLogger.Warn(watchCtx, "ai provider watch loop exited", slog.Error(err))
|
||||
}
|
||||
})
|
||||
defer func() {
|
||||
watchCancel()
|
||||
watchWG.Wait()
|
||||
}()
|
||||
|
||||
// The standalone listener is dedicated to Gateway traffic, so
|
||||
// the daemon is served at the root. The /api/v2/ai-gateway
|
||||
// and /api/v2/aibridge/ aliases are added for compatibility
|
||||
@@ -299,9 +315,8 @@ func resolveAIGatewayKey(key string, keyFile string) (string, error) {
|
||||
// reload is retried with backoff. A successful empty provider list is a valid
|
||||
// result and ends the loop.
|
||||
//
|
||||
// TODO(AIGOV-465): the standalone gateway has no provider-change refresh
|
||||
// trigger yet, so this runs once on startup; provider add/enable will not
|
||||
// propagate to a running standalone gateway.
|
||||
// Subsequent provider changes are delivered by WatchProviderReload, started
|
||||
// after this initial load returns.
|
||||
func loadProviders(ctx context.Context, reloader aibridged.ProviderReloader, logger slog.Logger, aibridgedDone <-chan struct{}) error {
|
||||
for r := retry.New(50*time.Millisecond, 10*time.Second); r.Wait(ctx); {
|
||||
if err := reloader.Reload(ctx); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user