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:
Danny Kopping
2026-07-08 15:32:17 +02:00
committed by GitHub
parent 48f07e6e13
commit affb359d13
16 changed files with 952 additions and 191 deletions
@@ -220,3 +220,18 @@ func (mr *MockDRPCClientMockRecorder) RecordToolUsage(ctx, in any) *gomock.Call
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordToolUsage", reflect.TypeOf((*MockDRPCClient)(nil).RecordToolUsage), ctx, in)
}
// WatchAIProviders mocks base method.
func (m *MockDRPCClient) WatchAIProviders(ctx context.Context, in *proto.WatchAIProvidersRequest) (proto.DRPCProviderConfigurator_WatchAIProvidersClient, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WatchAIProviders", ctx, in)
ret0, _ := ret[0].(proto.DRPCProviderConfigurator_WatchAIProvidersClient)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// WatchAIProviders indicates an expected call of WatchAIProviders.
func (mr *MockDRPCClientMockRecorder) WatchAIProviders(ctx, in any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchAIProviders", reflect.TypeOf((*MockDRPCClient)(nil).WatchAIProviders), ctx, in)
}
+5
View File
@@ -12,6 +12,11 @@ type Dialer func(ctx context.Context) (DRPCClient, error)
type ClientFunc func() (DRPCClient, error)
// ClientFuncWithContext acquires a DRPCClient, honoring the passed context so a
// blocking acquisition (e.g. waiting for the daemon to connect to coderd)
// unblocks when the context is canceled. Server.ClientContext satisfies it.
type ClientFuncWithContext func(context.Context) (DRPCClient, error)
// DRPCClient is the union of various service interfaces the client must support.
type DRPCClient interface {
proto.DRPCRecorderClient
+264 -150
View File
@@ -1481,6 +1481,83 @@ func (x *GetAIProvidersResponse) GetProviders() []*AIProvider {
return nil
}
type WatchAIProvidersRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *WatchAIProvidersRequest) Reset() {
*x = WatchAIProvidersRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *WatchAIProvidersRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WatchAIProvidersRequest) ProtoMessage() {}
func (x *WatchAIProvidersRequest) ProtoReflect() protoreflect.Message {
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[23]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use WatchAIProvidersRequest.ProtoReflect.Descriptor instead.
func (*WatchAIProvidersRequest) Descriptor() ([]byte, []int) {
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{23}
}
// WatchAIProvidersResponse is an intentionally empty change signal.
type WatchAIProvidersResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *WatchAIProvidersResponse) Reset() {
*x = WatchAIProvidersResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *WatchAIProvidersResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*WatchAIProvidersResponse) ProtoMessage() {}
func (x *WatchAIProvidersResponse) ProtoReflect() protoreflect.Message {
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[24]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use WatchAIProvidersResponse.ProtoReflect.Descriptor instead.
func (*WatchAIProvidersResponse) Descriptor() ([]byte, []int) {
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{24}
}
type AIProvider struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1500,7 +1577,7 @@ type AIProvider struct {
func (x *AIProvider) Reset() {
*x = AIProvider{}
if protoimpl.UnsafeEnabled {
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[23]
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1513,7 +1590,7 @@ func (x *AIProvider) String() string {
func (*AIProvider) ProtoMessage() {}
func (x *AIProvider) ProtoReflect() protoreflect.Message {
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[23]
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[25]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1526,7 +1603,7 @@ func (x *AIProvider) ProtoReflect() protoreflect.Message {
// Deprecated: Use AIProvider.ProtoReflect.Descriptor instead.
func (*AIProvider) Descriptor() ([]byte, []int) {
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{23}
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{25}
}
func (x *AIProvider) GetName() string {
@@ -1588,7 +1665,7 @@ type AIProviderKindBedrock struct {
func (x *AIProviderKindBedrock) Reset() {
*x = AIProviderKindBedrock{}
if protoimpl.UnsafeEnabled {
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[24]
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1601,7 +1678,7 @@ func (x *AIProviderKindBedrock) String() string {
func (*AIProviderKindBedrock) ProtoMessage() {}
func (x *AIProviderKindBedrock) ProtoReflect() protoreflect.Message {
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[24]
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[26]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1614,7 +1691,7 @@ func (x *AIProviderKindBedrock) ProtoReflect() protoreflect.Message {
// Deprecated: Use AIProviderKindBedrock.ProtoReflect.Descriptor instead.
func (*AIProviderKindBedrock) Descriptor() ([]byte, []int) {
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{24}
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{26}
}
func (x *AIProviderKindBedrock) GetRegion() string {
@@ -1944,104 +2021,113 @@ var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{
0x73, 0x65, 0x12, 0x2f, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18,
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49,
0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64,
0x65, 0x72, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x0a, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e,
0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61,
0x62, 0x6c, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c,
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12,
0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b,
0x65, 0x79, 0x73, 0x12, 0x36, 0x0a, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x18, 0x06,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49, 0x50,
0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, 0x6f,
0x63, 0x6b, 0x52, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x22, 0xf6, 0x01, 0x0a, 0x15,
0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65,
0x64, 0x72, 0x6f, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a,
0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x11,
0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65,
0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b,
0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65,
0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x28,
0x0a, 0x10, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x5f, 0x66, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64,
0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x46,
0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6c, 0x65,
0x5f, 0x61, 0x72, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x6c, 0x65,
0x41, 0x72, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f,
0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x49, 0x64, 0x32, 0xa9, 0x04, 0x0a, 0x08, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65,
0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72,
0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70,
0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x17,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26,
0x65, 0x72, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72,
0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x1a,
0x0a, 0x18, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb5, 0x01, 0x0a, 0x0a, 0x41,
0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a,
0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70,
0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01,
0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x62,
0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62,
0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05,
0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x36, 0x0a, 0x07, 0x62, 0x65,
0x64, 0x72, 0x6f, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4b, 0x69,
0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x52, 0x07, 0x62, 0x65, 0x64, 0x72, 0x6f,
0x63, 0x6b, 0x22, 0xf6, 0x01, 0x0a, 0x15, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
0x72, 0x4b, 0x69, 0x6e, 0x64, 0x42, 0x65, 0x64, 0x72, 0x6f, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06,
0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65,
0x67, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b,
0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73,
0x4b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65,
0x79, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f,
0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12,
0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x5f, 0x66,
0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0e, 0x73, 0x6d, 0x61, 0x6c, 0x6c, 0x46, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12,
0x19, 0x0a, 0x08, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x61, 0x72, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x72, 0x6f, 0x6c, 0x65, 0x41, 0x72, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78,
0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x32, 0xa9, 0x04, 0x0a, 0x08,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f,
0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74,
0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49,
0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x17, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74,
0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x12, 0x25,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74,
0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73,
0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73,
0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65,
0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50,
0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f,
0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52,
0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x12, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d,
0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x12, 0x20, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54,
0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65,
0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72,
0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65,
0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72,
0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76,
0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61,
0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d,
0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f,
0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53,
0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xaa,
0x01, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x72, 0x12, 0x47, 0x0a,
0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a,
0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x10, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67,
0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2e, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65,
0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2e, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65,
0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x65, 0x0a, 0x14, 0x50,
0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61,
0x74, 0x6f, 0x72, 0x12, 0x4d, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76,
0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65,
0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41,
0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f,
0x63, 0x6f, 0x64, 0x65, 0x72, 0x64, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6f, 0x6e,
0x45, 0x6e, 0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a,
0x10, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67,
0x65, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64,
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x56, 0x0a, 0x11, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d,
0x70, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61, 0x67,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x50, 0x72, 0x6f, 0x6d, 0x70, 0x74, 0x55, 0x73, 0x61,
0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x0f, 0x52, 0x65,
0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1d, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c,
0x55, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x54, 0x6f, 0x6f, 0x6c, 0x55,
0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x12,
0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67,
0x68, 0x74, 0x12, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x52, 0x65, 0x63,
0x6f, 0x72, 0x64, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x54, 0x68, 0x6f, 0x75, 0x67, 0x68, 0x74, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x01, 0x0a, 0x0f, 0x4d, 0x43, 0x50, 0x43,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x13, 0x47,
0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69,
0x67, 0x73, 0x12, 0x21, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43,
0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65,
0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7a, 0x0a, 0x1d, 0x47, 0x65, 0x74,
0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54,
0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41,
0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
0x47, 0x65, 0x74, 0x4d, 0x43, 0x50, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65,
0x73, 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x42, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xaa, 0x01, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
0x69, 0x7a, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x0c, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72,
0x69, 0x7a, 0x65, 0x64, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41,
0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x6f,
0x72, 0x69, 0x7a, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a,
0x10, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67, 0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65,
0x64, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67,
0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x49, 0x73, 0x42, 0x75, 0x64, 0x67,
0x65, 0x74, 0x45, 0x78, 0x63, 0x65, 0x65, 0x64, 0x65, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x32, 0xbc, 0x01, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43,
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x4d, 0x0a, 0x0e, 0x47,
0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69,
0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x10, 0x57, 0x61,
0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x1e,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72,
0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x41, 0x49, 0x50, 0x72,
0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30,
0x01, 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x63,
0x6f, 0x64, 0x65, 0x72, 0x64, 0x2f, 0x61, 0x69, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x64, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -2056,7 +2142,7 @@ func file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP() []byte {
return file_coderd_aibridged_proto_aibridged_proto_rawDescData
}
var file_coderd_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 32)
var file_coderd_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 34)
var file_coderd_aibridged_proto_aibridged_proto_goTypes = []interface{}{
(*RecordInterceptionRequest)(nil), // 0: proto.RecordInterceptionRequest
(*RecordInterceptionResponse)(nil), // 1: proto.RecordInterceptionResponse
@@ -2081,42 +2167,44 @@ var file_coderd_aibridged_proto_aibridged_proto_goTypes = []interface{}{
(*IsBudgetExceededResponse)(nil), // 20: proto.IsBudgetExceededResponse
(*GetAIProvidersRequest)(nil), // 21: proto.GetAIProvidersRequest
(*GetAIProvidersResponse)(nil), // 22: proto.GetAIProvidersResponse
(*AIProvider)(nil), // 23: proto.AIProvider
(*AIProviderKindBedrock)(nil), // 24: proto.AIProviderKindBedrock
nil, // 25: proto.RecordInterceptionRequest.MetadataEntry
nil, // 26: proto.RecordTokenUsageRequest.MetadataEntry
nil, // 27: proto.RecordPromptUsageRequest.MetadataEntry
nil, // 28: proto.RecordToolUsageRequest.MetadataEntry
nil, // 29: proto.RecordModelThoughtRequest.MetadataEntry
nil, // 30: proto.GetMCPServerAccessTokensBatchResponse.AccessTokensEntry
nil, // 31: proto.GetMCPServerAccessTokensBatchResponse.ErrorsEntry
(*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp
(*anypb.Any)(nil), // 33: google.protobuf.Any
(*WatchAIProvidersRequest)(nil), // 23: proto.WatchAIProvidersRequest
(*WatchAIProvidersResponse)(nil), // 24: proto.WatchAIProvidersResponse
(*AIProvider)(nil), // 25: proto.AIProvider
(*AIProviderKindBedrock)(nil), // 26: proto.AIProviderKindBedrock
nil, // 27: proto.RecordInterceptionRequest.MetadataEntry
nil, // 28: proto.RecordTokenUsageRequest.MetadataEntry
nil, // 29: proto.RecordPromptUsageRequest.MetadataEntry
nil, // 30: proto.RecordToolUsageRequest.MetadataEntry
nil, // 31: proto.RecordModelThoughtRequest.MetadataEntry
nil, // 32: proto.GetMCPServerAccessTokensBatchResponse.AccessTokensEntry
nil, // 33: proto.GetMCPServerAccessTokensBatchResponse.ErrorsEntry
(*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp
(*anypb.Any)(nil), // 35: google.protobuf.Any
}
var file_coderd_aibridged_proto_aibridged_proto_depIdxs = []int32{
25, // 0: proto.RecordInterceptionRequest.metadata:type_name -> proto.RecordInterceptionRequest.MetadataEntry
32, // 1: proto.RecordInterceptionRequest.started_at:type_name -> google.protobuf.Timestamp
32, // 2: proto.RecordInterceptionEndedRequest.ended_at:type_name -> google.protobuf.Timestamp
26, // 3: proto.RecordTokenUsageRequest.metadata:type_name -> proto.RecordTokenUsageRequest.MetadataEntry
32, // 4: proto.RecordTokenUsageRequest.created_at:type_name -> google.protobuf.Timestamp
27, // 5: proto.RecordPromptUsageRequest.metadata:type_name -> proto.RecordPromptUsageRequest.MetadataEntry
32, // 6: proto.RecordPromptUsageRequest.created_at:type_name -> google.protobuf.Timestamp
28, // 7: proto.RecordToolUsageRequest.metadata:type_name -> proto.RecordToolUsageRequest.MetadataEntry
32, // 8: proto.RecordToolUsageRequest.created_at:type_name -> google.protobuf.Timestamp
29, // 9: proto.RecordModelThoughtRequest.metadata:type_name -> proto.RecordModelThoughtRequest.MetadataEntry
32, // 10: proto.RecordModelThoughtRequest.created_at:type_name -> google.protobuf.Timestamp
27, // 0: proto.RecordInterceptionRequest.metadata:type_name -> proto.RecordInterceptionRequest.MetadataEntry
34, // 1: proto.RecordInterceptionRequest.started_at:type_name -> google.protobuf.Timestamp
34, // 2: proto.RecordInterceptionEndedRequest.ended_at:type_name -> google.protobuf.Timestamp
28, // 3: proto.RecordTokenUsageRequest.metadata:type_name -> proto.RecordTokenUsageRequest.MetadataEntry
34, // 4: proto.RecordTokenUsageRequest.created_at:type_name -> google.protobuf.Timestamp
29, // 5: proto.RecordPromptUsageRequest.metadata:type_name -> proto.RecordPromptUsageRequest.MetadataEntry
34, // 6: proto.RecordPromptUsageRequest.created_at:type_name -> google.protobuf.Timestamp
30, // 7: proto.RecordToolUsageRequest.metadata:type_name -> proto.RecordToolUsageRequest.MetadataEntry
34, // 8: proto.RecordToolUsageRequest.created_at:type_name -> google.protobuf.Timestamp
31, // 9: proto.RecordModelThoughtRequest.metadata:type_name -> proto.RecordModelThoughtRequest.MetadataEntry
34, // 10: proto.RecordModelThoughtRequest.created_at:type_name -> google.protobuf.Timestamp
14, // 11: proto.GetMCPServerConfigsResponse.coder_mcp_config:type_name -> proto.MCPServerConfig
14, // 12: proto.GetMCPServerConfigsResponse.external_auth_mcp_configs:type_name -> proto.MCPServerConfig
30, // 13: proto.GetMCPServerAccessTokensBatchResponse.access_tokens:type_name -> proto.GetMCPServerAccessTokensBatchResponse.AccessTokensEntry
31, // 14: proto.GetMCPServerAccessTokensBatchResponse.errors:type_name -> proto.GetMCPServerAccessTokensBatchResponse.ErrorsEntry
32, // 15: proto.IsBudgetExceededRequest.period_start:type_name -> google.protobuf.Timestamp
23, // 16: proto.GetAIProvidersResponse.providers:type_name -> proto.AIProvider
24, // 17: proto.AIProvider.bedrock:type_name -> proto.AIProviderKindBedrock
33, // 18: proto.RecordInterceptionRequest.MetadataEntry.value:type_name -> google.protobuf.Any
33, // 19: proto.RecordTokenUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
33, // 20: proto.RecordPromptUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
33, // 21: proto.RecordToolUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
33, // 22: proto.RecordModelThoughtRequest.MetadataEntry.value:type_name -> google.protobuf.Any
32, // 13: proto.GetMCPServerAccessTokensBatchResponse.access_tokens:type_name -> proto.GetMCPServerAccessTokensBatchResponse.AccessTokensEntry
33, // 14: proto.GetMCPServerAccessTokensBatchResponse.errors:type_name -> proto.GetMCPServerAccessTokensBatchResponse.ErrorsEntry
34, // 15: proto.IsBudgetExceededRequest.period_start:type_name -> google.protobuf.Timestamp
25, // 16: proto.GetAIProvidersResponse.providers:type_name -> proto.AIProvider
26, // 17: proto.AIProvider.bedrock:type_name -> proto.AIProviderKindBedrock
35, // 18: proto.RecordInterceptionRequest.MetadataEntry.value:type_name -> google.protobuf.Any
35, // 19: proto.RecordTokenUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
35, // 20: proto.RecordPromptUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
35, // 21: proto.RecordToolUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
35, // 22: proto.RecordModelThoughtRequest.MetadataEntry.value:type_name -> google.protobuf.Any
0, // 23: proto.Recorder.RecordInterception:input_type -> proto.RecordInterceptionRequest
2, // 24: proto.Recorder.RecordInterceptionEnded:input_type -> proto.RecordInterceptionEndedRequest
4, // 25: proto.Recorder.RecordTokenUsage:input_type -> proto.RecordTokenUsageRequest
@@ -2128,19 +2216,21 @@ var file_coderd_aibridged_proto_aibridged_proto_depIdxs = []int32{
17, // 31: proto.Authorizer.IsAuthorized:input_type -> proto.IsAuthorizedRequest
19, // 32: proto.Authorizer.IsBudgetExceeded:input_type -> proto.IsBudgetExceededRequest
21, // 33: proto.ProviderConfigurator.GetAIProviders:input_type -> proto.GetAIProvidersRequest
1, // 34: proto.Recorder.RecordInterception:output_type -> proto.RecordInterceptionResponse
3, // 35: proto.Recorder.RecordInterceptionEnded:output_type -> proto.RecordInterceptionEndedResponse
5, // 36: proto.Recorder.RecordTokenUsage:output_type -> proto.RecordTokenUsageResponse
7, // 37: proto.Recorder.RecordPromptUsage:output_type -> proto.RecordPromptUsageResponse
9, // 38: proto.Recorder.RecordToolUsage:output_type -> proto.RecordToolUsageResponse
11, // 39: proto.Recorder.RecordModelThought:output_type -> proto.RecordModelThoughtResponse
13, // 40: proto.MCPConfigurator.GetMCPServerConfigs:output_type -> proto.GetMCPServerConfigsResponse
16, // 41: proto.MCPConfigurator.GetMCPServerAccessTokensBatch:output_type -> proto.GetMCPServerAccessTokensBatchResponse
18, // 42: proto.Authorizer.IsAuthorized:output_type -> proto.IsAuthorizedResponse
20, // 43: proto.Authorizer.IsBudgetExceeded:output_type -> proto.IsBudgetExceededResponse
22, // 44: proto.ProviderConfigurator.GetAIProviders:output_type -> proto.GetAIProvidersResponse
34, // [34:45] is the sub-list for method output_type
23, // [23:34] is the sub-list for method input_type
23, // 34: proto.ProviderConfigurator.WatchAIProviders:input_type -> proto.WatchAIProvidersRequest
1, // 35: proto.Recorder.RecordInterception:output_type -> proto.RecordInterceptionResponse
3, // 36: proto.Recorder.RecordInterceptionEnded:output_type -> proto.RecordInterceptionEndedResponse
5, // 37: proto.Recorder.RecordTokenUsage:output_type -> proto.RecordTokenUsageResponse
7, // 38: proto.Recorder.RecordPromptUsage:output_type -> proto.RecordPromptUsageResponse
9, // 39: proto.Recorder.RecordToolUsage:output_type -> proto.RecordToolUsageResponse
11, // 40: proto.Recorder.RecordModelThought:output_type -> proto.RecordModelThoughtResponse
13, // 41: proto.MCPConfigurator.GetMCPServerConfigs:output_type -> proto.GetMCPServerConfigsResponse
16, // 42: proto.MCPConfigurator.GetMCPServerAccessTokensBatch:output_type -> proto.GetMCPServerAccessTokensBatchResponse
18, // 43: proto.Authorizer.IsAuthorized:output_type -> proto.IsAuthorizedResponse
20, // 44: proto.Authorizer.IsBudgetExceeded:output_type -> proto.IsBudgetExceededResponse
22, // 45: proto.ProviderConfigurator.GetAIProviders:output_type -> proto.GetAIProvidersResponse
24, // 46: proto.ProviderConfigurator.WatchAIProviders:output_type -> proto.WatchAIProvidersResponse
35, // [35:47] is the sub-list for method output_type
23, // [23:35] is the sub-list for method input_type
23, // [23:23] is the sub-list for extension type_name
23, // [23:23] is the sub-list for extension extendee
0, // [0:23] is the sub-list for field type_name
@@ -2429,7 +2519,7 @@ func file_coderd_aibridged_proto_aibridged_proto_init() {
}
}
file_coderd_aibridged_proto_aibridged_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AIProvider); i {
switch v := v.(*WatchAIProvidersRequest); i {
case 0:
return &v.state
case 1:
@@ -2441,6 +2531,30 @@ func file_coderd_aibridged_proto_aibridged_proto_init() {
}
}
file_coderd_aibridged_proto_aibridged_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WatchAIProvidersResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_coderd_aibridged_proto_aibridged_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AIProvider); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_coderd_aibridged_proto_aibridged_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AIProviderKindBedrock); i {
case 0:
return &v.state
@@ -2462,7 +2576,7 @@ func file_coderd_aibridged_proto_aibridged_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_coderd_aibridged_proto_aibridged_proto_rawDesc,
NumEnums: 0,
NumMessages: 32,
NumMessages: 34,
NumExtensions: 0,
NumServices: 4,
},
+11
View File
@@ -45,6 +45,12 @@ service ProviderConfigurator {
// GetAIProviders returns the full provider set (enabled and disabled).
// It synchronizes with provider seeding so the response is never raced.
rpc GetAIProviders(GetAIProvidersRequest) returns (GetAIProvidersResponse);
// WatchAIProviders streams a signal whenever the provider set changes
// (env seed completion or a CRUD add/update/delete). The signal carries no
// payload; on each message the client refetches via GetAIProviders. The
// server emits one signal immediately on subscribe so a client that
// connected after the last change still converges.
rpc WatchAIProviders(WatchAIProvidersRequest) returns (stream WatchAIProvidersResponse);
}
message RecordInterceptionRequest {
@@ -198,6 +204,11 @@ message GetAIProvidersResponse {
repeated AIProvider providers = 1;
}
message WatchAIProvidersRequest {}
// WatchAIProvidersResponse is an intentionally empty change signal.
message WatchAIProvidersResponse {}
message AIProvider {
string name = 1;
string type = 2;
+69 -1
View File
@@ -544,6 +544,7 @@ type DRPCProviderConfiguratorClient interface {
DRPCConn() drpc.Conn
GetAIProviders(ctx context.Context, in *GetAIProvidersRequest) (*GetAIProvidersResponse, error)
WatchAIProviders(ctx context.Context, in *WatchAIProvidersRequest) (DRPCProviderConfigurator_WatchAIProvidersClient, error)
}
type drpcProviderConfiguratorClient struct {
@@ -565,8 +566,49 @@ func (c *drpcProviderConfiguratorClient) GetAIProviders(ctx context.Context, in
return out, nil
}
func (c *drpcProviderConfiguratorClient) WatchAIProviders(ctx context.Context, in *WatchAIProvidersRequest) (DRPCProviderConfigurator_WatchAIProvidersClient, error) {
stream, err := c.cc.NewStream(ctx, "/proto.ProviderConfigurator/WatchAIProviders", drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{})
if err != nil {
return nil, err
}
x := &drpcProviderConfigurator_WatchAIProvidersClient{stream}
if err := x.MsgSend(in, drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{}); err != nil {
return nil, err
}
if err := x.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type DRPCProviderConfigurator_WatchAIProvidersClient interface {
drpc.Stream
Recv() (*WatchAIProvidersResponse, error)
}
type drpcProviderConfigurator_WatchAIProvidersClient struct {
drpc.Stream
}
func (x *drpcProviderConfigurator_WatchAIProvidersClient) GetStream() drpc.Stream {
return x.Stream
}
func (x *drpcProviderConfigurator_WatchAIProvidersClient) Recv() (*WatchAIProvidersResponse, error) {
m := new(WatchAIProvidersResponse)
if err := x.MsgRecv(m, drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{}); err != nil {
return nil, err
}
return m, nil
}
func (x *drpcProviderConfigurator_WatchAIProvidersClient) RecvMsg(m *WatchAIProvidersResponse) error {
return x.MsgRecv(m, drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{})
}
type DRPCProviderConfiguratorServer interface {
GetAIProviders(context.Context, *GetAIProvidersRequest) (*GetAIProvidersResponse, error)
WatchAIProviders(*WatchAIProvidersRequest, DRPCProviderConfigurator_WatchAIProvidersStream) error
}
type DRPCProviderConfiguratorUnimplementedServer struct{}
@@ -575,9 +617,13 @@ func (s *DRPCProviderConfiguratorUnimplementedServer) GetAIProviders(context.Con
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCProviderConfiguratorUnimplementedServer) WatchAIProviders(*WatchAIProvidersRequest, DRPCProviderConfigurator_WatchAIProvidersStream) error {
return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
type DRPCProviderConfiguratorDescription struct{}
func (DRPCProviderConfiguratorDescription) NumMethods() int { return 1 }
func (DRPCProviderConfiguratorDescription) NumMethods() int { return 2 }
func (DRPCProviderConfiguratorDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
switch n {
@@ -590,6 +636,15 @@ func (DRPCProviderConfiguratorDescription) Method(n int) (string, drpc.Encoding,
in1.(*GetAIProvidersRequest),
)
}, DRPCProviderConfiguratorServer.GetAIProviders, true
case 1:
return "/proto.ProviderConfigurator/WatchAIProviders", drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return nil, srv.(DRPCProviderConfiguratorServer).
WatchAIProviders(
in1.(*WatchAIProvidersRequest),
&drpcProviderConfigurator_WatchAIProvidersStream{in2.(drpc.Stream)},
)
}, DRPCProviderConfiguratorServer.WatchAIProviders, true
default:
return "", nil, nil, nil, false
}
@@ -614,3 +669,16 @@ func (x *drpcProviderConfigurator_GetAIProvidersStream) SendAndClose(m *GetAIPro
}
return x.CloseSend()
}
type DRPCProviderConfigurator_WatchAIProvidersStream interface {
drpc.Stream
Send(*WatchAIProvidersResponse) error
}
type drpcProviderConfigurator_WatchAIProvidersStream struct {
drpc.Stream
}
func (x *drpcProviderConfigurator_WatchAIProvidersStream) Send(m *WatchAIProvidersResponse) error {
return x.MsgSend(m, drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{})
}
+6 -1
View File
@@ -12,9 +12,14 @@ import "github.com/coder/coder/v2/apiversion"
// - Adds the ProviderConfigurator service with the GetAIProviders unary RPC,
// letting embedded and standalone gateways fetch provider configuration
// over DRPC instead of reading the database directly.
//
// API v1.2:
// - Adds the ProviderConfigurator.WatchAIProviders streaming RPC, pushing a
// change signal to gateways so a running standalone gateway refetches its
// provider set when the provider configuration changes.
const (
CurrentMajor = 1
CurrentMinor = 1
CurrentMinor = 2
)
// VersionQueryParam is the URL query parameter the standalone AI Gateway
+82 -2
View File
@@ -2,12 +2,15 @@ package aibridged
import (
"context"
"time"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/aibridged/proto"
dbpubsub "github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/pubsub"
"github.com/coder/retry"
)
// ProviderReloader refreshes a component's provider snapshot.
@@ -21,7 +24,10 @@ type ProviderReloader interface {
// event is missed.
//
// A subscription failure returns an error without reloading. The initial
// reload is best-effort: a reload failure is logged and not returned.
// reload is best-effort: a reload failure is logged and not returned. A
// dropped-message delivery error triggers a reload too, matching
// WatchAIProviders: a drop may have masked a change, so the snapshot must
// reconverge.
func SubscribeProviderReload(
ctx context.Context,
ps dbpubsub.Pubsub,
@@ -37,8 +43,9 @@ func SubscribeProviderReload(
unsubscribe, err := ps.SubscribeWithErr(pubsub.AIProvidersChangedChannel, func(cbCtx context.Context, _ []byte, err error) {
if err != nil {
// A dropped message may have masked a change, so reload anyway to
// reconverge rather than skipping.
logger.Warn(cbCtx, "ai providers changed event delivered with error", slog.Error(err))
return
}
if err := reloader.Reload(cbCtx); err != nil {
logger.Warn(cbCtx, "reload ai provider snapshot from pubsub event", slog.Error(err))
@@ -55,3 +62,76 @@ func SubscribeProviderReload(
}
return unsubscribe, nil
}
// WatchProviderReload opens a coderd WatchAIProviders stream via clientFn and
// calls reloader.Reload on each change signal the server emits. The stream is
// re-established with exponential backoff whenever it drops. It does not
// perform an initial load; the caller is responsible for any blocking load
// before serving.
//
// It runs until ctx is canceled, then returns ctx.Err(). clientFn receives ctx,
// so a client acquisition that blocks (e.g. Server.ClientContext waiting for
// the daemon to connect to coderd) unblocks when ctx is canceled, leaving no
// goroutine behind.
func WatchProviderReload(
ctx context.Context,
clientFn ClientFuncWithContext,
reloader ProviderReloader,
logger slog.Logger,
) error {
if clientFn == nil {
return xerrors.New("client is required")
}
if reloader == nil {
return xerrors.New("reloader is required")
}
r := retry.New(50*time.Millisecond, 10*time.Second)
for {
received, err := watchProviderReloadOnce(ctx, clientFn, reloader, logger)
if ctx.Err() != nil {
return ctx.Err()
}
logger.Warn(ctx, "ai provider watch stream ended; reconnecting", slog.Error(err))
// Only reset the backoff once a signal was actually received. A stream
// that opens but errors before any Recv (e.g. the server fails during
// subscribe) would otherwise reset to the floor and reconnect at
// network-RTT speed.
if received {
r.Reset()
}
if !r.Wait(ctx) {
return ctx.Err()
}
}
}
// watchProviderReloadOnce opens a single WatchAIProviders stream and reloads on
// each signal until the stream fails. received reports whether at least one
// signal was received before the error.
func watchProviderReloadOnce(ctx context.Context, clientFn ClientFuncWithContext, reloader ProviderReloader, logger slog.Logger) (received bool, err error) {
// clientFn blocks until the daemon connects to coderd or ctx is canceled.
c, err := clientFn(ctx)
if err != nil {
return false, xerrors.Errorf("get ai-gateway client: %w", err)
}
stream, err := c.WatchAIProviders(ctx, &proto.WatchAIProvidersRequest{})
if err != nil {
return false, xerrors.Errorf("open ai providers watch stream: %w", err)
}
defer func() {
_ = stream.Close()
}()
for {
if _, err := stream.Recv(); err != nil {
return received, xerrors.Errorf("receive ai providers change signal: %w", err)
}
received = true
if err := reloader.Reload(ctx); err != nil {
logger.Warn(ctx, "failed to reload ai provider snapshot from watch signal", slog.Error(err))
continue
}
logger.Debug(ctx, "reloaded ai provider snapshot from watch signal")
}
}
+229 -3
View File
@@ -2,14 +2,20 @@ package aibridged_test
import (
"context"
"io"
"sync"
"sync/atomic"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"golang.org/x/xerrors"
"storj.io/drpc"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/aibridged"
"github.com/coder/coder/v2/coderd/aibridged/aibridgedmock"
"github.com/coder/coder/v2/coderd/aibridged/proto"
dbpubsub "github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/pubsub"
"github.com/coder/coder/v2/testutil"
@@ -77,7 +83,7 @@ func TestSubscribeProviderReloadFailsWhenSubscribeFails(t *testing.T) {
require.Equal(t, 0, calls.count())
}
func TestSubscribeProviderReloadIgnoresEventError(t *testing.T) {
func TestSubscribeProviderReloadReloadsOnEventError(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
@@ -92,13 +98,215 @@ func TestSubscribeProviderReloadIgnoresEventError(t *testing.T) {
require.Equal(t, 1, calls.count())
// A dropped-message delivery error may have masked a change, so it must
// still trigger a reload to reconverge.
ps.listener(ctx, nil, errPubsubDelivery)
require.Equal(t, 1, calls.count())
require.Equal(t, 2, calls.count())
ps.listener(ctx, nil, nil)
require.Equal(t, 2, calls.count())
require.Equal(t, 3, calls.count())
}
func TestWatchProviderReload(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
logger := slogtest.Make(t, nil)
ctrl := gomock.NewController(t)
mockClient := aibridgedmock.NewMockDRPCClient(ctrl)
// A single stream delivers two change signals, then blocks on its context
// until the watch is canceled.
events := make(chan error, 2)
events <- nil
events <- nil
mockClient.EXPECT().WatchAIProviders(gomock.Any(), gomock.Any()).DoAndReturn(
func(rpcCtx context.Context, _ *proto.WatchAIProvidersRequest) (proto.DRPCProviderConfigurator_WatchAIProvidersClient, error) {
return &fakeWatchClientStream{ctx: rpcCtx, events: events}, nil
}).AnyTimes()
calls := &recordingReloader{}
clientFunc := func(context.Context) (aibridged.DRPCClient, error) { return mockClient, nil }
watchCtx, watchCancel := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- aibridged.WatchProviderReload(watchCtx, clientFunc, calls, logger) }()
require.Eventually(t, func() bool { return calls.count() >= 2 }, testutil.WaitShort, testutil.IntervalFast,
"each change signal must trigger a reload")
watchCancel()
require.ErrorIs(t, testutil.TryReceive(ctx, t, done), context.Canceled)
}
func TestWatchProviderReloadReconnects(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
logger := slogtest.Make(t, nil)
ctrl := gomock.NewController(t)
mockClient := aibridgedmock.NewMockDRPCClient(ctrl)
// The first stream delivers one signal then drops; subsequent streams
// deliver one signal then block. WatchProviderReload must reconnect after
// the drop and keep reloading.
var attempt atomic.Int32
mockClient.EXPECT().WatchAIProviders(gomock.Any(), gomock.Any()).DoAndReturn(
func(rpcCtx context.Context, _ *proto.WatchAIProvidersRequest) (proto.DRPCProviderConfigurator_WatchAIProvidersClient, error) {
ev := make(chan error, 2)
if attempt.Add(1) == 1 {
ev <- nil
ev <- io.EOF
} else {
ev <- nil
}
return &fakeWatchClientStream{ctx: rpcCtx, events: ev}, nil
}).AnyTimes()
calls := &recordingReloader{}
clientFunc := func(context.Context) (aibridged.DRPCClient, error) { return mockClient, nil }
watchCtx, watchCancel := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- aibridged.WatchProviderReload(watchCtx, clientFunc, calls, logger) }()
require.Eventually(t, func() bool { return calls.count() >= 2 }, testutil.WaitShort, testutil.IntervalFast,
"reload must continue after the stream drops and reconnects")
watchCancel()
require.ErrorIs(t, testutil.TryReceive(ctx, t, done), context.Canceled)
}
func TestWatchProviderReloadCancelUnblocksClient(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
logger := slogtest.Make(t, nil)
// clientFn blocks until its context is canceled, modeling
// Server.ClientContext waiting for the daemon to connect to coderd. Only
// watchCancel is exercised (no stream activity, no daemon close), so the
// loop can return only if clientFn honors the context it receives.
var once sync.Once
entered := make(chan struct{})
clientFunc := func(clientCtx context.Context) (aibridged.DRPCClient, error) {
once.Do(func() { close(entered) })
<-clientCtx.Done()
return nil, clientCtx.Err()
}
watchCtx, watchCancel := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- aibridged.WatchProviderReload(watchCtx, clientFunc, &recordingReloader{}, logger) }()
testutil.TryReceive(ctx, t, entered)
watchCancel()
require.ErrorIs(t, testutil.TryReceive(ctx, t, done), context.Canceled)
}
func TestWatchProviderReloadRetriesDialFailure(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
logger := slogtest.Make(t, nil)
ctrl := gomock.NewController(t)
mockClient := aibridgedmock.NewMockDRPCClient(ctrl)
// Once dialed, the stream delivers one signal then blocks on its context.
mockClient.EXPECT().WatchAIProviders(gomock.Any(), gomock.Any()).DoAndReturn(
func(rpcCtx context.Context, _ *proto.WatchAIProvidersRequest) (proto.DRPCProviderConfigurator_WatchAIProvidersClient, error) {
ev := make(chan error, 1)
ev <- nil
return &fakeWatchClientStream{ctx: rpcCtx, events: ev}, nil
}).AnyTimes()
calls := &recordingReloader{}
// The first dial fails; the second succeeds, and the loop must keep
// retrying until the dial succeeds and a reload fires.
var attempt atomic.Int32
clientFunc := func(context.Context) (aibridged.DRPCClient, error) {
if attempt.Add(1) == 1 {
return nil, xerrors.New("dial failed")
}
return mockClient, nil
}
watchCtx, watchCancel := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- aibridged.WatchProviderReload(watchCtx, clientFunc, calls, logger) }()
require.Eventually(t, func() bool { return calls.count() >= 1 }, testutil.WaitShort, testutil.IntervalFast,
"reload must fire only after a failed dial is retried successfully")
require.GreaterOrEqual(t, int(attempt.Load()), 2, "the first dial must have failed and been retried")
watchCancel()
require.ErrorIs(t, testutil.TryReceive(ctx, t, done), context.Canceled)
}
func TestWatchProviderReloadContinuesAfterReloadError(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitMedium)
logger := slogtest.Make(t, nil)
ctrl := gomock.NewController(t)
mockClient := aibridgedmock.NewMockDRPCClient(ctrl)
events := make(chan error, 3)
for range 3 {
events <- nil
}
mockClient.EXPECT().WatchAIProviders(gomock.Any(), gomock.Any()).DoAndReturn(
func(rpcCtx context.Context, _ *proto.WatchAIProvidersRequest) (proto.DRPCProviderConfigurator_WatchAIProvidersClient, error) {
return &fakeWatchClientStream{ctx: rpcCtx, events: events}, nil
}).AnyTimes()
// Fails its first two reloads, then succeeds.
reloader := &failNReloader{n: 2}
clientFunc := func(context.Context) (aibridged.DRPCClient, error) { return mockClient, nil }
watchCtx, watchCancel := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- aibridged.WatchProviderReload(watchCtx, clientFunc, reloader, logger) }()
require.Eventually(t, func() bool { return reloader.count() >= 3 }, testutil.WaitShort, testutil.IntervalFast,
"a failed reload must not stop the watch loop")
watchCancel()
require.ErrorIs(t, testutil.TryReceive(ctx, t, done), context.Canceled)
}
// fakeWatchClientStream is a minimal
// proto.DRPCProviderConfigurator_WatchAIProvidersClient. Each value popped from
// events either yields a change signal (nil) or returns the given error; when
// events is empty Recv blocks until the stream context is canceled.
type fakeWatchClientStream struct {
ctx context.Context
events chan error
}
func (s *fakeWatchClientStream) Recv() (*proto.WatchAIProvidersResponse, error) {
select {
case err := <-s.events:
if err != nil {
return nil, err
}
return &proto.WatchAIProvidersResponse{}, nil
case <-s.ctx.Done():
return nil, s.ctx.Err()
}
}
func (s *fakeWatchClientStream) Context() context.Context { return s.ctx }
func (*fakeWatchClientStream) MsgSend(drpc.Message, drpc.Encoding) error { return nil }
func (*fakeWatchClientStream) MsgRecv(drpc.Message, drpc.Encoding) error { return nil }
func (*fakeWatchClientStream) CloseSend() error { return nil }
func (*fakeWatchClientStream) Close() error { return nil }
// recordingReloader is a minimal [aibridged.ProviderReloader] that
// counts calls.
type recordingReloader struct {
@@ -118,6 +326,24 @@ func (r *recordingReloader) count() int {
return int(r.n.Load())
}
// failNReloader fails its first n Reload calls, then succeeds, counting all
// calls.
type failNReloader struct {
n int32
calls atomic.Int32
}
func (r *failNReloader) Reload(_ context.Context) error {
if r.calls.Add(1) <= r.n {
return errReloadFailed
}
return nil
}
func (r *failNReloader) count() int {
return int(r.calls.Load())
}
var (
errReloadFailed = stubError("reload failed")
errPubsubDelivery = stubError("pubsub delivery failed")