mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat: fetch providers over DRPC (#26650)
Closes [AIGOV-455](https://linear.app/codercom/issue/AIGOV-455/extend-drpc-with-buildproviders). ## Why The AI Gateway (`aibridged`) is being split into a standalone process that must not touch the database. `coderd` stays the source of truth and seeds the `ai_providers` / `ai_provider_keys` tables from the environment. This PR adds a DRPC call so the gateway fetches provider config from `coderd` instead of reading the DB, for both the embedded and standalone daemons. ## What - **Proto:** new `ProviderConfigurator` service with a unary `GetAIProviders` RPC, plus `AIProvider` / `AIProviderBedrock` messages. `CurrentMinor` bumped to 1 (additive). - **Server (`coderd/aibridgedserver`):** `GetAIProviders` runs a read-only `InTx` under `LockIDAIProvidersEnvSeed` so it never returns a mid-seed snapshot, reads providers (incl. disabled) plus keys for enabled ones, and maps to proto under `dbauthz.AsAIBridged`. Unmappable rows are skipped and logged; plaintext keys and Bedrock secrets are never logged. - **Client:** `DRPCProviderConfiguratorClient` wired into the client union, `dialer.go`, and `CreateInMemoryAIBridgeServer`. - **cli:** `BuildProvidersFromProto` maps the response through the existing DB-neutral `buildProvider`. A shared `poolRPCReloader` does the fetch/build/replace for both daemons: the embedded daemon reloads on every `ai_providers` change and fails startup if it cannot subscribe; the standalone gateway drives the same reloader once at startup, retrying until success and staying interruptible. - **Dead code removed:** `BuildProvidersFromConfig`, `ProvidersFromConfig`, `AIProviderFromConfig`, and the DB-read `BuildProviders` path.
This commit is contained in:
+5
-4
@@ -103,9 +103,10 @@ func (api *API) CreateInMemoryAIBridgeServer(dialCtx context.Context) (client ai
|
||||
}()
|
||||
|
||||
return &aibridged.Client{
|
||||
Conn: clientSession,
|
||||
DRPCRecorderClient: aibridgedproto.NewDRPCRecorderClient(clientSession),
|
||||
DRPCMCPConfiguratorClient: aibridgedproto.NewDRPCMCPConfiguratorClient(clientSession),
|
||||
DRPCAuthorizerClient: aibridgedproto.NewDRPCAuthorizerClient(clientSession),
|
||||
Conn: clientSession,
|
||||
DRPCRecorderClient: aibridgedproto.NewDRPCRecorderClient(clientSession),
|
||||
DRPCMCPConfiguratorClient: aibridgedproto.NewDRPCMCPConfiguratorClient(clientSession),
|
||||
DRPCAuthorizerClient: aibridgedproto.NewDRPCAuthorizerClient(clientSession),
|
||||
DRPCProviderConfiguratorClient: aibridgedproto.NewDRPCProviderConfiguratorClient(clientSession),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -108,8 +108,10 @@ connectLoop:
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: log this with INFO level when we implement external aibridge daemons.
|
||||
logConnect(s.lifecycleCtx, "successfully connected to coderd")
|
||||
// Logged at info so operators of standalone (external) gateways
|
||||
// can see initial connection and reconnection after a dial
|
||||
// failure (paired with the warning logged above).
|
||||
s.logger.Info(s.lifecycleCtx, "successfully connected to coderd")
|
||||
retrier.Reset()
|
||||
s.initConnectionOnce.Do(func() {
|
||||
close(s.initConnectionCh)
|
||||
|
||||
@@ -56,6 +56,21 @@ func (mr *MockDRPCClientMockRecorder) DRPCConn() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DRPCConn", reflect.TypeOf((*MockDRPCClient)(nil).DRPCConn))
|
||||
}
|
||||
|
||||
// GetAIProviders mocks base method.
|
||||
func (m *MockDRPCClient) GetAIProviders(ctx context.Context, in *proto.GetAIProvidersRequest) (*proto.GetAIProvidersResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviders", ctx, in)
|
||||
ret0, _ := ret[0].(*proto.GetAIProvidersResponse)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviders indicates an expected call of GetAIProviders.
|
||||
func (mr *MockDRPCClientMockRecorder) GetAIProviders(ctx, in any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviders", reflect.TypeOf((*MockDRPCClient)(nil).GetAIProviders), ctx, in)
|
||||
}
|
||||
|
||||
// GetMCPServerAccessTokensBatch mocks base method.
|
||||
func (m *MockDRPCClient) GetMCPServerAccessTokensBatch(ctx context.Context, in *proto.GetMCPServerAccessTokensBatchRequest) (*proto.GetMCPServerAccessTokensBatchResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -17,6 +17,7 @@ type DRPCClient interface {
|
||||
proto.DRPCRecorderClient
|
||||
proto.DRPCMCPConfiguratorClient
|
||||
proto.DRPCAuthorizerClient
|
||||
proto.DRPCProviderConfiguratorClient
|
||||
}
|
||||
|
||||
var _ DRPCClient = &Client{}
|
||||
@@ -25,6 +26,7 @@ type Client struct {
|
||||
proto.DRPCRecorderClient
|
||||
proto.DRPCMCPConfiguratorClient
|
||||
proto.DRPCAuthorizerClient
|
||||
proto.DRPCProviderConfiguratorClient
|
||||
|
||||
Conn drpc.Conn
|
||||
}
|
||||
|
||||
@@ -1268,6 +1268,268 @@ func (x *IsAuthorizedResponse) GetUsername() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetAIProvidersRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
}
|
||||
|
||||
func (x *GetAIProvidersRequest) Reset() {
|
||||
*x = GetAIProvidersRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[19]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetAIProvidersRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetAIProvidersRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetAIProvidersRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[19]
|
||||
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 GetAIProvidersRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetAIProvidersRequest) Descriptor() ([]byte, []int) {
|
||||
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{19}
|
||||
}
|
||||
|
||||
type GetAIProvidersResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Providers []*AIProvider `protobuf:"bytes,1,rep,name=providers,proto3" json:"providers,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetAIProvidersResponse) Reset() {
|
||||
*x = GetAIProvidersResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[20]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetAIProvidersResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetAIProvidersResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetAIProvidersResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[20]
|
||||
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 GetAIProvidersResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetAIProvidersResponse) Descriptor() ([]byte, []int) {
|
||||
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{20}
|
||||
}
|
||||
|
||||
func (x *GetAIProvidersResponse) GetProviders() []*AIProvider {
|
||||
if x != nil {
|
||||
return x.Providers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AIProvider struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
|
||||
Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"`
|
||||
BaseUrl string `protobuf:"bytes,4,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty"`
|
||||
// keys carries bearer API keys, populated only for enabled providers.
|
||||
Keys []string `protobuf:"bytes,5,rep,name=keys,proto3" json:"keys,omitempty"`
|
||||
// bedrock is populated when the provider's settings include Bedrock
|
||||
// credentials (regardless of provider type).
|
||||
Bedrock *AIProviderKindBedrock `protobuf:"bytes,6,opt,name=bedrock,proto3" json:"bedrock,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AIProvider) Reset() {
|
||||
*x = AIProvider{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[21]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AIProvider) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AIProvider) ProtoMessage() {}
|
||||
|
||||
func (x *AIProvider) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[21]
|
||||
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 AIProvider.ProtoReflect.Descriptor instead.
|
||||
func (*AIProvider) Descriptor() ([]byte, []int) {
|
||||
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{21}
|
||||
}
|
||||
|
||||
func (x *AIProvider) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AIProvider) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AIProvider) GetEnabled() bool {
|
||||
if x != nil {
|
||||
return x.Enabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *AIProvider) GetBaseUrl() string {
|
||||
if x != nil {
|
||||
return x.BaseUrl
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AIProvider) GetKeys() []string {
|
||||
if x != nil {
|
||||
return x.Keys
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *AIProvider) GetBedrock() *AIProviderKindBedrock {
|
||||
if x != nil {
|
||||
return x.Bedrock
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AIProviderKindBedrock struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Region string `protobuf:"bytes,1,opt,name=region,proto3" json:"region,omitempty"`
|
||||
AccessKey string `protobuf:"bytes,2,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
|
||||
AccessKeySecret string `protobuf:"bytes,3,opt,name=access_key_secret,json=accessKeySecret,proto3" json:"access_key_secret,omitempty"`
|
||||
Model string `protobuf:"bytes,4,opt,name=model,proto3" json:"model,omitempty"`
|
||||
SmallFastModel string `protobuf:"bytes,5,opt,name=small_fast_model,json=smallFastModel,proto3" json:"small_fast_model,omitempty"`
|
||||
RoleArn string `protobuf:"bytes,6,opt,name=role_arn,json=roleArn,proto3" json:"role_arn,omitempty"`
|
||||
}
|
||||
|
||||
func (x *AIProviderKindBedrock) Reset() {
|
||||
*x = AIProviderKindBedrock{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[22]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *AIProviderKindBedrock) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AIProviderKindBedrock) ProtoMessage() {}
|
||||
|
||||
func (x *AIProviderKindBedrock) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_coderd_aibridged_proto_aibridged_proto_msgTypes[22]
|
||||
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 AIProviderKindBedrock.ProtoReflect.Descriptor instead.
|
||||
func (*AIProviderKindBedrock) Descriptor() ([]byte, []int) {
|
||||
return file_coderd_aibridged_proto_aibridged_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
func (x *AIProviderKindBedrock) GetRegion() string {
|
||||
if x != nil {
|
||||
return x.Region
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AIProviderKindBedrock) GetAccessKey() string {
|
||||
if x != nil {
|
||||
return x.AccessKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AIProviderKindBedrock) GetAccessKeySecret() string {
|
||||
if x != nil {
|
||||
return x.AccessKeySecret
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AIProviderKindBedrock) GetModel() string {
|
||||
if x != nil {
|
||||
return x.Model
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AIProviderKindBedrock) GetSmallFastModel() string {
|
||||
if x != nil {
|
||||
return x.SmallFastModel
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AIProviderKindBedrock) GetRoleArn() string {
|
||||
if x != nil {
|
||||
return x.RoleArn
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_coderd_aibridged_proto_aibridged_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{
|
||||
@@ -1523,65 +1785,103 @@ var file_coderd_aibridged_proto_aibridged_proto_rawDesc = []byte{
|
||||
0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x4b,
|
||||
0x65, 0x79, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65,
|
||||
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, 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, 0x55, 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, 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,
|
||||
0x22, 0x17, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
|
||||
0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x49, 0x0a, 0x16, 0x47, 0x65, 0x74,
|
||||
0x41, 0x49, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 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, 0xd5, 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, 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,
|
||||
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, 0x55,
|
||||
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, 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,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -1596,7 +1896,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, 26)
|
||||
var file_coderd_aibridged_proto_aibridged_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
|
||||
var file_coderd_aibridged_proto_aibridged_proto_goTypes = []interface{}{
|
||||
(*RecordInterceptionRequest)(nil), // 0: proto.RecordInterceptionRequest
|
||||
(*RecordInterceptionResponse)(nil), // 1: proto.RecordInterceptionResponse
|
||||
@@ -1617,60 +1917,68 @@ var file_coderd_aibridged_proto_aibridged_proto_goTypes = []interface{}{
|
||||
(*GetMCPServerAccessTokensBatchResponse)(nil), // 16: proto.GetMCPServerAccessTokensBatchResponse
|
||||
(*IsAuthorizedRequest)(nil), // 17: proto.IsAuthorizedRequest
|
||||
(*IsAuthorizedResponse)(nil), // 18: proto.IsAuthorizedResponse
|
||||
nil, // 19: proto.RecordInterceptionRequest.MetadataEntry
|
||||
nil, // 20: proto.RecordTokenUsageRequest.MetadataEntry
|
||||
nil, // 21: proto.RecordPromptUsageRequest.MetadataEntry
|
||||
nil, // 22: proto.RecordToolUsageRequest.MetadataEntry
|
||||
nil, // 23: proto.RecordModelThoughtRequest.MetadataEntry
|
||||
nil, // 24: proto.GetMCPServerAccessTokensBatchResponse.AccessTokensEntry
|
||||
nil, // 25: proto.GetMCPServerAccessTokensBatchResponse.ErrorsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 26: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 27: google.protobuf.Any
|
||||
(*GetAIProvidersRequest)(nil), // 19: proto.GetAIProvidersRequest
|
||||
(*GetAIProvidersResponse)(nil), // 20: proto.GetAIProvidersResponse
|
||||
(*AIProvider)(nil), // 21: proto.AIProvider
|
||||
(*AIProviderKindBedrock)(nil), // 22: proto.AIProviderKindBedrock
|
||||
nil, // 23: proto.RecordInterceptionRequest.MetadataEntry
|
||||
nil, // 24: proto.RecordTokenUsageRequest.MetadataEntry
|
||||
nil, // 25: proto.RecordPromptUsageRequest.MetadataEntry
|
||||
nil, // 26: proto.RecordToolUsageRequest.MetadataEntry
|
||||
nil, // 27: proto.RecordModelThoughtRequest.MetadataEntry
|
||||
nil, // 28: proto.GetMCPServerAccessTokensBatchResponse.AccessTokensEntry
|
||||
nil, // 29: proto.GetMCPServerAccessTokensBatchResponse.ErrorsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 30: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 31: google.protobuf.Any
|
||||
}
|
||||
var file_coderd_aibridged_proto_aibridged_proto_depIdxs = []int32{
|
||||
19, // 0: proto.RecordInterceptionRequest.metadata:type_name -> proto.RecordInterceptionRequest.MetadataEntry
|
||||
26, // 1: proto.RecordInterceptionRequest.started_at:type_name -> google.protobuf.Timestamp
|
||||
26, // 2: proto.RecordInterceptionEndedRequest.ended_at:type_name -> google.protobuf.Timestamp
|
||||
20, // 3: proto.RecordTokenUsageRequest.metadata:type_name -> proto.RecordTokenUsageRequest.MetadataEntry
|
||||
26, // 4: proto.RecordTokenUsageRequest.created_at:type_name -> google.protobuf.Timestamp
|
||||
21, // 5: proto.RecordPromptUsageRequest.metadata:type_name -> proto.RecordPromptUsageRequest.MetadataEntry
|
||||
26, // 6: proto.RecordPromptUsageRequest.created_at:type_name -> google.protobuf.Timestamp
|
||||
22, // 7: proto.RecordToolUsageRequest.metadata:type_name -> proto.RecordToolUsageRequest.MetadataEntry
|
||||
26, // 8: proto.RecordToolUsageRequest.created_at:type_name -> google.protobuf.Timestamp
|
||||
23, // 9: proto.RecordModelThoughtRequest.metadata:type_name -> proto.RecordModelThoughtRequest.MetadataEntry
|
||||
26, // 10: proto.RecordModelThoughtRequest.created_at:type_name -> google.protobuf.Timestamp
|
||||
23, // 0: proto.RecordInterceptionRequest.metadata:type_name -> proto.RecordInterceptionRequest.MetadataEntry
|
||||
30, // 1: proto.RecordInterceptionRequest.started_at:type_name -> google.protobuf.Timestamp
|
||||
30, // 2: proto.RecordInterceptionEndedRequest.ended_at:type_name -> google.protobuf.Timestamp
|
||||
24, // 3: proto.RecordTokenUsageRequest.metadata:type_name -> proto.RecordTokenUsageRequest.MetadataEntry
|
||||
30, // 4: proto.RecordTokenUsageRequest.created_at:type_name -> google.protobuf.Timestamp
|
||||
25, // 5: proto.RecordPromptUsageRequest.metadata:type_name -> proto.RecordPromptUsageRequest.MetadataEntry
|
||||
30, // 6: proto.RecordPromptUsageRequest.created_at:type_name -> google.protobuf.Timestamp
|
||||
26, // 7: proto.RecordToolUsageRequest.metadata:type_name -> proto.RecordToolUsageRequest.MetadataEntry
|
||||
30, // 8: proto.RecordToolUsageRequest.created_at:type_name -> google.protobuf.Timestamp
|
||||
27, // 9: proto.RecordModelThoughtRequest.metadata:type_name -> proto.RecordModelThoughtRequest.MetadataEntry
|
||||
30, // 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
|
||||
24, // 13: proto.GetMCPServerAccessTokensBatchResponse.access_tokens:type_name -> proto.GetMCPServerAccessTokensBatchResponse.AccessTokensEntry
|
||||
25, // 14: proto.GetMCPServerAccessTokensBatchResponse.errors:type_name -> proto.GetMCPServerAccessTokensBatchResponse.ErrorsEntry
|
||||
27, // 15: proto.RecordInterceptionRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
27, // 16: proto.RecordTokenUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
27, // 17: proto.RecordPromptUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
27, // 18: proto.RecordToolUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
27, // 19: proto.RecordModelThoughtRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
0, // 20: proto.Recorder.RecordInterception:input_type -> proto.RecordInterceptionRequest
|
||||
2, // 21: proto.Recorder.RecordInterceptionEnded:input_type -> proto.RecordInterceptionEndedRequest
|
||||
4, // 22: proto.Recorder.RecordTokenUsage:input_type -> proto.RecordTokenUsageRequest
|
||||
6, // 23: proto.Recorder.RecordPromptUsage:input_type -> proto.RecordPromptUsageRequest
|
||||
8, // 24: proto.Recorder.RecordToolUsage:input_type -> proto.RecordToolUsageRequest
|
||||
10, // 25: proto.Recorder.RecordModelThought:input_type -> proto.RecordModelThoughtRequest
|
||||
12, // 26: proto.MCPConfigurator.GetMCPServerConfigs:input_type -> proto.GetMCPServerConfigsRequest
|
||||
15, // 27: proto.MCPConfigurator.GetMCPServerAccessTokensBatch:input_type -> proto.GetMCPServerAccessTokensBatchRequest
|
||||
17, // 28: proto.Authorizer.IsAuthorized:input_type -> proto.IsAuthorizedRequest
|
||||
1, // 29: proto.Recorder.RecordInterception:output_type -> proto.RecordInterceptionResponse
|
||||
3, // 30: proto.Recorder.RecordInterceptionEnded:output_type -> proto.RecordInterceptionEndedResponse
|
||||
5, // 31: proto.Recorder.RecordTokenUsage:output_type -> proto.RecordTokenUsageResponse
|
||||
7, // 32: proto.Recorder.RecordPromptUsage:output_type -> proto.RecordPromptUsageResponse
|
||||
9, // 33: proto.Recorder.RecordToolUsage:output_type -> proto.RecordToolUsageResponse
|
||||
11, // 34: proto.Recorder.RecordModelThought:output_type -> proto.RecordModelThoughtResponse
|
||||
13, // 35: proto.MCPConfigurator.GetMCPServerConfigs:output_type -> proto.GetMCPServerConfigsResponse
|
||||
16, // 36: proto.MCPConfigurator.GetMCPServerAccessTokensBatch:output_type -> proto.GetMCPServerAccessTokensBatchResponse
|
||||
18, // 37: proto.Authorizer.IsAuthorized:output_type -> proto.IsAuthorizedResponse
|
||||
29, // [29:38] is the sub-list for method output_type
|
||||
20, // [20:29] is the sub-list for method input_type
|
||||
20, // [20:20] is the sub-list for extension type_name
|
||||
20, // [20:20] is the sub-list for extension extendee
|
||||
0, // [0:20] is the sub-list for field type_name
|
||||
28, // 13: proto.GetMCPServerAccessTokensBatchResponse.access_tokens:type_name -> proto.GetMCPServerAccessTokensBatchResponse.AccessTokensEntry
|
||||
29, // 14: proto.GetMCPServerAccessTokensBatchResponse.errors:type_name -> proto.GetMCPServerAccessTokensBatchResponse.ErrorsEntry
|
||||
21, // 15: proto.GetAIProvidersResponse.providers:type_name -> proto.AIProvider
|
||||
22, // 16: proto.AIProvider.bedrock:type_name -> proto.AIProviderKindBedrock
|
||||
31, // 17: proto.RecordInterceptionRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
31, // 18: proto.RecordTokenUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
31, // 19: proto.RecordPromptUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
31, // 20: proto.RecordToolUsageRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
31, // 21: proto.RecordModelThoughtRequest.MetadataEntry.value:type_name -> google.protobuf.Any
|
||||
0, // 22: proto.Recorder.RecordInterception:input_type -> proto.RecordInterceptionRequest
|
||||
2, // 23: proto.Recorder.RecordInterceptionEnded:input_type -> proto.RecordInterceptionEndedRequest
|
||||
4, // 24: proto.Recorder.RecordTokenUsage:input_type -> proto.RecordTokenUsageRequest
|
||||
6, // 25: proto.Recorder.RecordPromptUsage:input_type -> proto.RecordPromptUsageRequest
|
||||
8, // 26: proto.Recorder.RecordToolUsage:input_type -> proto.RecordToolUsageRequest
|
||||
10, // 27: proto.Recorder.RecordModelThought:input_type -> proto.RecordModelThoughtRequest
|
||||
12, // 28: proto.MCPConfigurator.GetMCPServerConfigs:input_type -> proto.GetMCPServerConfigsRequest
|
||||
15, // 29: proto.MCPConfigurator.GetMCPServerAccessTokensBatch:input_type -> proto.GetMCPServerAccessTokensBatchRequest
|
||||
17, // 30: proto.Authorizer.IsAuthorized:input_type -> proto.IsAuthorizedRequest
|
||||
19, // 31: proto.ProviderConfigurator.GetAIProviders:input_type -> proto.GetAIProvidersRequest
|
||||
1, // 32: proto.Recorder.RecordInterception:output_type -> proto.RecordInterceptionResponse
|
||||
3, // 33: proto.Recorder.RecordInterceptionEnded:output_type -> proto.RecordInterceptionEndedResponse
|
||||
5, // 34: proto.Recorder.RecordTokenUsage:output_type -> proto.RecordTokenUsageResponse
|
||||
7, // 35: proto.Recorder.RecordPromptUsage:output_type -> proto.RecordPromptUsageResponse
|
||||
9, // 36: proto.Recorder.RecordToolUsage:output_type -> proto.RecordToolUsageResponse
|
||||
11, // 37: proto.Recorder.RecordModelThought:output_type -> proto.RecordModelThoughtResponse
|
||||
13, // 38: proto.MCPConfigurator.GetMCPServerConfigs:output_type -> proto.GetMCPServerConfigsResponse
|
||||
16, // 39: proto.MCPConfigurator.GetMCPServerAccessTokensBatch:output_type -> proto.GetMCPServerAccessTokensBatchResponse
|
||||
18, // 40: proto.Authorizer.IsAuthorized:output_type -> proto.IsAuthorizedResponse
|
||||
20, // 41: proto.ProviderConfigurator.GetAIProviders:output_type -> proto.GetAIProvidersResponse
|
||||
32, // [32:42] is the sub-list for method output_type
|
||||
22, // [22:32] is the sub-list for method input_type
|
||||
22, // [22:22] is the sub-list for extension type_name
|
||||
22, // [22:22] is the sub-list for extension extendee
|
||||
0, // [0:22] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_coderd_aibridged_proto_aibridged_proto_init() }
|
||||
@@ -1907,6 +2215,54 @@ func file_coderd_aibridged_proto_aibridged_proto_init() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_coderd_aibridged_proto_aibridged_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetAIProvidersRequest); 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[20].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetAIProvidersResponse); 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[21].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[22].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*AIProviderKindBedrock); 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[0].OneofWrappers = []interface{}{}
|
||||
file_coderd_aibridged_proto_aibridged_proto_msgTypes[8].OneofWrappers = []interface{}{}
|
||||
@@ -1916,9 +2272,9 @@ func file_coderd_aibridged_proto_aibridged_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_coderd_aibridged_proto_aibridged_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 26,
|
||||
NumMessages: 30,
|
||||
NumExtensions: 0,
|
||||
NumServices: 3,
|
||||
NumServices: 4,
|
||||
},
|
||||
GoTypes: file_coderd_aibridged_proto_aibridged_proto_goTypes,
|
||||
DependencyIndexes: file_coderd_aibridged_proto_aibridged_proto_depIdxs,
|
||||
|
||||
@@ -36,6 +36,14 @@ service Authorizer {
|
||||
rpc IsAuthorized(IsAuthorizedRequest) returns (IsAuthorizedResponse);
|
||||
}
|
||||
|
||||
// ProviderConfigurator serves AI provider configuration to embedded and
|
||||
// standalone AI Gateway daemons. The database is the single source of truth.
|
||||
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);
|
||||
}
|
||||
|
||||
message RecordInterceptionRequest {
|
||||
string id = 1; // UUID.
|
||||
string initiator_id = 2; // UUID.
|
||||
@@ -159,3 +167,30 @@ message IsAuthorizedResponse {
|
||||
string api_key_id = 2;
|
||||
string username = 3;
|
||||
}
|
||||
|
||||
message GetAIProvidersRequest {}
|
||||
|
||||
message GetAIProvidersResponse {
|
||||
repeated AIProvider providers = 1;
|
||||
}
|
||||
|
||||
message AIProvider {
|
||||
string name = 1;
|
||||
string type = 2;
|
||||
bool enabled = 3;
|
||||
string base_url = 4;
|
||||
// keys carries bearer API keys, populated only for enabled providers.
|
||||
repeated string keys = 5;
|
||||
// bedrock is populated when the provider's settings include Bedrock
|
||||
// credentials (regardless of provider type).
|
||||
AIProviderKindBedrock bedrock = 6;
|
||||
}
|
||||
|
||||
message AIProviderKindBedrock {
|
||||
string region = 1;
|
||||
string access_key = 2;
|
||||
string access_key_secret = 3;
|
||||
string model = 4;
|
||||
string small_fast_model = 5;
|
||||
string role_arn = 6;
|
||||
}
|
||||
|
||||
@@ -499,3 +499,78 @@ func (x *drpcAuthorizer_IsAuthorizedStream) SendAndClose(m *IsAuthorizedResponse
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
type DRPCProviderConfiguratorClient interface {
|
||||
DRPCConn() drpc.Conn
|
||||
|
||||
GetAIProviders(ctx context.Context, in *GetAIProvidersRequest) (*GetAIProvidersResponse, error)
|
||||
}
|
||||
|
||||
type drpcProviderConfiguratorClient struct {
|
||||
cc drpc.Conn
|
||||
}
|
||||
|
||||
func NewDRPCProviderConfiguratorClient(cc drpc.Conn) DRPCProviderConfiguratorClient {
|
||||
return &drpcProviderConfiguratorClient{cc}
|
||||
}
|
||||
|
||||
func (c *drpcProviderConfiguratorClient) DRPCConn() drpc.Conn { return c.cc }
|
||||
|
||||
func (c *drpcProviderConfiguratorClient) GetAIProviders(ctx context.Context, in *GetAIProvidersRequest) (*GetAIProvidersResponse, error) {
|
||||
out := new(GetAIProvidersResponse)
|
||||
err := c.cc.Invoke(ctx, "/proto.ProviderConfigurator/GetAIProviders", drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{}, in, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type DRPCProviderConfiguratorServer interface {
|
||||
GetAIProviders(context.Context, *GetAIProvidersRequest) (*GetAIProvidersResponse, error)
|
||||
}
|
||||
|
||||
type DRPCProviderConfiguratorUnimplementedServer struct{}
|
||||
|
||||
func (s *DRPCProviderConfiguratorUnimplementedServer) GetAIProviders(context.Context, *GetAIProvidersRequest) (*GetAIProvidersResponse, error) {
|
||||
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
|
||||
}
|
||||
|
||||
type DRPCProviderConfiguratorDescription struct{}
|
||||
|
||||
func (DRPCProviderConfiguratorDescription) NumMethods() int { return 1 }
|
||||
|
||||
func (DRPCProviderConfiguratorDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
|
||||
switch n {
|
||||
case 0:
|
||||
return "/proto.ProviderConfigurator/GetAIProviders", drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{},
|
||||
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
|
||||
return srv.(DRPCProviderConfiguratorServer).
|
||||
GetAIProviders(
|
||||
ctx,
|
||||
in1.(*GetAIProvidersRequest),
|
||||
)
|
||||
}, DRPCProviderConfiguratorServer.GetAIProviders, true
|
||||
default:
|
||||
return "", nil, nil, nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func DRPCRegisterProviderConfigurator(mux drpc.Mux, impl DRPCProviderConfiguratorServer) error {
|
||||
return mux.Register(impl, DRPCProviderConfiguratorDescription{})
|
||||
}
|
||||
|
||||
type DRPCProviderConfigurator_GetAIProvidersStream interface {
|
||||
drpc.Stream
|
||||
SendAndClose(*GetAIProvidersResponse) error
|
||||
}
|
||||
|
||||
type drpcProviderConfigurator_GetAIProvidersStream struct {
|
||||
drpc.Stream
|
||||
}
|
||||
|
||||
func (x *drpcProviderConfigurator_GetAIProvidersStream) SendAndClose(m *GetAIProvidersResponse) error {
|
||||
if err := x.MsgSend(m, drpcEncoding_File_coderd_aibridged_proto_aibridged_proto{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return x.CloseSend()
|
||||
}
|
||||
|
||||
@@ -7,9 +7,14 @@ import "github.com/coder/coder/v2/apiversion"
|
||||
// API v1.0:
|
||||
// - Initial version. Serves the Recorder, MCPConfigurator, and Authorizer
|
||||
// services to embedded and standalone AI Gateway daemons.
|
||||
//
|
||||
// API v1.1:
|
||||
// - 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.
|
||||
const (
|
||||
CurrentMajor = 1
|
||||
CurrentMinor = 0
|
||||
CurrentMinor = 1
|
||||
)
|
||||
|
||||
// CurrentVersion is the current aibridged API version.
|
||||
|
||||
@@ -15,7 +15,13 @@ type ProviderReloader interface {
|
||||
Reload(ctx context.Context) error
|
||||
}
|
||||
|
||||
// SubscribeProviderReload refreshes once, then on AI provider changes.
|
||||
// SubscribeProviderReload subscribes to AI provider change events, reloading
|
||||
// the reloader's snapshot on each event, and performs one initial reload
|
||||
// before returning. Subscribing happens before the initial reload so no change
|
||||
// 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.
|
||||
func SubscribeProviderReload(
|
||||
ctx context.Context,
|
||||
ps dbpubsub.Pubsub,
|
||||
@@ -43,6 +49,7 @@ func SubscribeProviderReload(
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("subscribe to %s: %w", pubsub.AIProvidersChangedChannel, err)
|
||||
}
|
||||
|
||||
if err := reloader.Reload(ctx); err != nil {
|
||||
logger.Warn(ctx, "initial ai provider reload", slog.Error(err))
|
||||
}
|
||||
|
||||
@@ -59,6 +59,24 @@ func TestSubscribeProviderReloadSurfacesReloadError(t *testing.T) {
|
||||
"Reload must keep firing even after a previous Reload returned an error")
|
||||
}
|
||||
|
||||
func TestSubscribeProviderReloadFailsWhenSubscribeFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
logger := slogtest.Make(t, nil)
|
||||
ps := &subscribeErrPubsub{}
|
||||
|
||||
calls := &recordingReloader{}
|
||||
unsub, err := aibridged.SubscribeProviderReload(ctx, ps, calls, logger)
|
||||
require.Error(t, err, "a subscription failure must be surfaced to the caller")
|
||||
require.Nil(t, unsub)
|
||||
|
||||
// Without a subscription the snapshot can never track changes, so the
|
||||
// caller must fail; no reload is attempted.
|
||||
require.Equal(t, 0, calls.count())
|
||||
}
|
||||
|
||||
func TestSubscribeProviderReloadIgnoresEventError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -131,3 +149,25 @@ func (*errInjectingPubsub) Publish(string, []byte) error {
|
||||
func (*errInjectingPubsub) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ dbpubsub.Pubsub = &subscribeErrPubsub{}
|
||||
|
||||
// subscribeErrPubsub fails every subscription attempt, exercising the path
|
||||
// where SubscribeProviderReload cannot establish a subscription.
|
||||
type subscribeErrPubsub struct{}
|
||||
|
||||
func (*subscribeErrPubsub) Subscribe(string, dbpubsub.Listener) (func(), error) {
|
||||
return nil, xerrors.New("Subscribe not implemented")
|
||||
}
|
||||
|
||||
func (*subscribeErrPubsub) SubscribeWithErr(string, dbpubsub.ListenerWithErr) (func(), error) {
|
||||
return nil, xerrors.New("subscribe failed")
|
||||
}
|
||||
|
||||
func (*subscribeErrPubsub) Publish(string, []byte) error {
|
||||
return xerrors.New("Publish not implemented")
|
||||
}
|
||||
|
||||
func (*subscribeErrPubsub) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ type DRPCServer interface {
|
||||
proto.DRPCRecorderServer
|
||||
proto.DRPCMCPConfiguratorServer
|
||||
proto.DRPCAuthorizerServer
|
||||
proto.DRPCProviderConfiguratorServer
|
||||
}
|
||||
|
||||
@@ -21,11 +21,13 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/aiseats"
|
||||
"github.com/coder/coder/v2/coderd/apikey"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/externalauth"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
codermcp "github.com/coder/coder/v2/coderd/mcp"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@@ -80,6 +82,13 @@ type store interface {
|
||||
// Authorizer-related queries.
|
||||
GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error)
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (database.User, error)
|
||||
|
||||
// ProviderConfigurator-related queries. InTx wraps the provider and key
|
||||
// reads in a single read-only transaction; AcquireLock serializes against
|
||||
// any in-flight env seed holding LockIDAIProvidersEnvSeed.
|
||||
InTx(func(database.Store) error, *database.TxOptions) error
|
||||
GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error)
|
||||
GetAIProviderKeysByProviderIDs(ctx context.Context, providerIDs []uuid.UUID) ([]database.AIProviderKey, error)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
@@ -682,6 +691,93 @@ func (s *Server) IsAuthorized(ctx context.Context, in *proto.IsAuthorizedRequest
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAIProviders returns the full AI provider set (enabled and disabled) from
|
||||
// the database, which is the single source of truth seeded from coderd's
|
||||
// environment. Embedded and standalone AI Gateway daemons call this over DRPC
|
||||
// to build their provider pool instead of reading the database directly.
|
||||
//
|
||||
// The handler reads under a read-only transaction that first acquires
|
||||
// LockIDAIProvidersEnvSeed, so it blocks until any in-flight env seed commits
|
||||
// or rolls back. This guarantees the response is never a partial, mid-seed
|
||||
// snapshot.
|
||||
//
|
||||
// Keys are populated only for enabled providers; disabled providers never call
|
||||
// upstream, so their secrets are withheld.
|
||||
//
|
||||
// SECURITY: the response carries plaintext API keys and Bedrock credentials.
|
||||
// Do not log the response struct.
|
||||
func (s *Server) GetAIProviders(ctx context.Context, _ *proto.GetAIProvidersRequest) (*proto.GetAIProvidersResponse, error) {
|
||||
//nolint:gocritic // AIBridged has a minimal permission set scoped to AI Bridge queries.
|
||||
ctx = dbauthz.AsAIBridged(ctx)
|
||||
|
||||
var (
|
||||
rows []database.AIProvider
|
||||
keysByProvider map[uuid.UUID][]database.AIProviderKey
|
||||
)
|
||||
// Wrap both reads in a read-only transaction so the provider list and the
|
||||
// key list are consistent with each other, and so the seed lock is held
|
||||
// for the duration of the reads.
|
||||
err := s.store.InTx(func(tx database.Store) error {
|
||||
// Block on any in-flight seed transaction holding the advisory lock so
|
||||
// the response reflects a fully-seeded snapshot.
|
||||
if err := tx.AcquireLock(ctx, database.LockIDAIProvidersEnvSeed); err != nil {
|
||||
return xerrors.Errorf("acquire ai providers env seed lock: %w", err)
|
||||
}
|
||||
|
||||
var err error
|
||||
rows, err = tx.GetAIProviders(ctx, database.GetAIProvidersParams{IncludeDisabled: true})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get ai providers: %w", err)
|
||||
}
|
||||
|
||||
// Load keys only for enabled providers to avoid materializing secrets
|
||||
// for disabled rows.
|
||||
ids := make([]uuid.UUID, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if !row.Enabled {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, row.ID)
|
||||
}
|
||||
keysByProvider = make(map[uuid.UUID][]database.AIProviderKey, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
keyRows, err := tx.GetAIProviderKeysByProviderIDs(ctx, ids)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get ai provider keys: %w", err)
|
||||
}
|
||||
for _, k := range keyRows {
|
||||
keysByProvider[k.ProviderID] = append(keysByProvider[k.ProviderID], k)
|
||||
}
|
||||
return nil
|
||||
}, &database.TxOptions{ReadOnly: true, TxIdentifier: "get_ai_providers"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers := make([]*proto.AIProvider, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
p, err := aiProviderToProto(row, keysByProvider[row.ID])
|
||||
if err != nil {
|
||||
// Skip the offending row rather than failing the whole fetch:
|
||||
// one row with a corrupt settings blob must not break provider
|
||||
// configuration for every gateway, which would otherwise loop
|
||||
// forever on the empty pool.
|
||||
s.logger.Error(ctx, "skipping ai provider with invalid settings; it will be absent from the gateway pool",
|
||||
slog.F("provider_id", row.ID),
|
||||
slog.F("provider_name", row.Name),
|
||||
slog.F("provider_type", string(row.Type)),
|
||||
slog.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
providers = append(providers, p)
|
||||
}
|
||||
|
||||
return &proto.GetAIProvidersResponse{Providers: providers}, nil
|
||||
}
|
||||
|
||||
// Deprecated: Injected MCP in AI Bridge is deprecated and will be removed in a future release.
|
||||
func getCoderMCPServerConfig(experiments codersdk.Experiments, accessURL string) (*proto.MCPServerConfig, error) {
|
||||
// Both the MCP & OAuth2 experiments are currently required in order to use our
|
||||
@@ -751,3 +847,44 @@ func parseOptionalInt32(n *int32) sql.NullInt32 {
|
||||
}
|
||||
return sql.NullInt32{Int32: *n, Valid: true}
|
||||
}
|
||||
|
||||
// aiProviderToProto maps a single ai_providers row (and its keys, for enabled
|
||||
// providers) to the proto representation served to AI Gateway daemons. Keys and
|
||||
// Bedrock settings are only attached for enabled providers; disabled providers
|
||||
// never call upstream so their secrets are withheld.
|
||||
func aiProviderToProto(row database.AIProvider, keys []database.AIProviderKey) (*proto.AIProvider, error) {
|
||||
p := &proto.AIProvider{
|
||||
Name: row.Name,
|
||||
Type: string(row.Type),
|
||||
Enabled: row.Enabled,
|
||||
BaseUrl: row.BaseUrl,
|
||||
}
|
||||
// Disabled providers are rendered as stubs by the client and never call
|
||||
// upstream, so only the identity fields are returned; keys and settings
|
||||
// (including Bedrock credentials) are withheld.
|
||||
if !row.Enabled {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
p.Keys = make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
p.Keys = append(p.Keys, k.APIKey)
|
||||
}
|
||||
|
||||
settings, err := db2sdk.AIProviderSettings(row.Settings)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("decode settings: %w", err)
|
||||
}
|
||||
if settings.Bedrock != nil {
|
||||
p.Bedrock = &proto.AIProviderKindBedrock{
|
||||
Region: settings.Bedrock.Region,
|
||||
AccessKey: ptr.NilToEmpty(settings.Bedrock.AccessKey),
|
||||
AccessKeySecret: ptr.NilToEmpty(settings.Bedrock.AccessKeySecret),
|
||||
Model: settings.Bedrock.Model,
|
||||
SmallFastModel: settings.Bedrock.SmallFastModel,
|
||||
RoleArn: settings.Bedrock.RoleARN,
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"cdr.dev/slog/v3/sloggers/slogjson"
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/coderd/aibridged"
|
||||
"github.com/coder/coder/v2/coderd/aibridged/proto"
|
||||
"github.com/coder/coder/v2/coderd/aibridgedserver"
|
||||
@@ -2405,3 +2407,207 @@ func TestInferredThreadsByToolCalls(t *testing.T) {
|
||||
require.Equal(t, uuid.NullUUID{UUID: bID, Valid: true}, intcC.ThreadParentID)
|
||||
require.Equal(t, uuid.NullUUID{UUID: aID, Valid: true}, intcC.ThreadRootID)
|
||||
}
|
||||
|
||||
// TestGetAIProviders exercises the row-to-proto mapping over a real database:
|
||||
// enabled providers carry their keys (and typed Bedrock settings), disabled
|
||||
// providers are included but withhold keys and settings, Copilot (a keyless
|
||||
// BYOK provider) round-trips with no keys, and an enabled provider whose
|
||||
// settings blob cannot be decoded is skipped rather than failing the fetch.
|
||||
func TestGetAIProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
// The skipped misconfigured provider is logged at Error level by design,
|
||||
// so error logs are expected here.
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
|
||||
|
||||
// Enabled OpenAI with two keys.
|
||||
openai := dbgen.AIProvider(t, db, database.AIProvider{
|
||||
Type: database.AIProviderTypeOpenai,
|
||||
Name: "openai",
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.openai.com/",
|
||||
})
|
||||
dbgen.AIProviderKey(t, db, database.AIProviderKey{ProviderID: openai.ID, APIKey: "sk-openai-1"})
|
||||
dbgen.AIProviderKey(t, db, database.AIProviderKey{ProviderID: openai.ID, APIKey: "sk-openai-2"})
|
||||
|
||||
// Enabled Bedrock with typed settings.
|
||||
bedrockSettings, err := json.Marshal(codersdk.AIProviderSettings{
|
||||
Bedrock: &codersdk.AIProviderBedrockSettings{
|
||||
Region: "us-east-1",
|
||||
Model: "anthropic.claude-3",
|
||||
SmallFastModel: "anthropic.claude-haiku",
|
||||
AccessKey: ptr.Ref("AKID"),
|
||||
AccessKeySecret: ptr.Ref("secret"),
|
||||
RoleARN: "arn:aws:iam::123456789012:role/bedrock",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
dbgen.AIProvider(t, db, database.AIProvider{
|
||||
Type: database.AIProviderTypeBedrock,
|
||||
Name: "bedrock",
|
||||
Enabled: true,
|
||||
BaseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com/",
|
||||
Settings: sql.NullString{String: string(bedrockSettings), Valid: true},
|
||||
})
|
||||
|
||||
// Enabled Copilot, which is keyless (BYOK per request).
|
||||
dbgen.AIProvider(t, db, database.AIProvider{
|
||||
Type: database.AIProviderTypeCopilot,
|
||||
Name: "copilot",
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.githubcopilot.com/",
|
||||
})
|
||||
|
||||
// Disabled Anthropic with a key; the key must be withheld.
|
||||
disabled := dbgen.AIProvider(t, db, database.AIProvider{
|
||||
Type: database.AIProviderTypeAnthropic,
|
||||
Name: "anthropic-off",
|
||||
BaseUrl: "https://api.anthropic.com/",
|
||||
}, func(p *database.InsertAIProviderParams) {
|
||||
p.Enabled = false
|
||||
})
|
||||
dbgen.AIProviderKey(t, db, database.AIProviderKey{ProviderID: disabled.ID, APIKey: "sk-secret"})
|
||||
|
||||
// Enabled provider with an undecodable settings blob; it must be skipped
|
||||
// so one corrupt row does not break provider config for every gateway.
|
||||
dbgen.AIProvider(t, db, database.AIProvider{
|
||||
Type: database.AIProviderTypeBedrock,
|
||||
Name: "broken-settings",
|
||||
Enabled: true,
|
||||
BaseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com/",
|
||||
Settings: sql.NullString{String: "{not valid json", Valid: true},
|
||||
})
|
||||
|
||||
srv, err := aibridgedserver.NewServer(ctx, db, logger, "/", codersdk.AIBridgeConfig{}, nil, nil, agplaiseats.Noop{})
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := srv.GetAIProviders(ctx, &proto.GetAIProvidersRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
byName := make(map[string]*proto.AIProvider, len(resp.GetProviders()))
|
||||
for _, p := range resp.GetProviders() {
|
||||
byName[p.GetName()] = p
|
||||
}
|
||||
require.Len(t, byName, 4)
|
||||
assert.NotContains(t, byName, "broken-settings", "provider with undecodable settings must be skipped")
|
||||
|
||||
gotOpenAI := byName["openai"]
|
||||
require.NotNil(t, gotOpenAI)
|
||||
assert.True(t, gotOpenAI.GetEnabled())
|
||||
assert.Equal(t, string(database.AIProviderTypeOpenai), gotOpenAI.GetType())
|
||||
assert.Equal(t, "https://api.openai.com/", gotOpenAI.GetBaseUrl())
|
||||
assert.ElementsMatch(t, []string{"sk-openai-1", "sk-openai-2"}, gotOpenAI.GetKeys())
|
||||
assert.Nil(t, gotOpenAI.GetBedrock())
|
||||
|
||||
gotBedrock := byName["bedrock"]
|
||||
require.NotNil(t, gotBedrock)
|
||||
assert.True(t, gotBedrock.GetEnabled())
|
||||
require.NotNil(t, gotBedrock.GetBedrock())
|
||||
assert.Equal(t, "us-east-1", gotBedrock.GetBedrock().GetRegion())
|
||||
assert.Equal(t, "anthropic.claude-3", gotBedrock.GetBedrock().GetModel())
|
||||
assert.Equal(t, "anthropic.claude-haiku", gotBedrock.GetBedrock().GetSmallFastModel())
|
||||
assert.Equal(t, "AKID", gotBedrock.GetBedrock().GetAccessKey())
|
||||
assert.Equal(t, "secret", gotBedrock.GetBedrock().GetAccessKeySecret())
|
||||
assert.Equal(t, "arn:aws:iam::123456789012:role/bedrock", gotBedrock.GetBedrock().GetRoleArn())
|
||||
|
||||
gotCopilot := byName["copilot"]
|
||||
require.NotNil(t, gotCopilot)
|
||||
assert.True(t, gotCopilot.GetEnabled())
|
||||
assert.Empty(t, gotCopilot.GetKeys())
|
||||
|
||||
gotDisabled := byName["anthropic-off"]
|
||||
require.NotNil(t, gotDisabled)
|
||||
assert.False(t, gotDisabled.GetEnabled())
|
||||
assert.Empty(t, gotDisabled.GetKeys(), "keys must be withheld for disabled providers")
|
||||
assert.Nil(t, gotDisabled.GetBedrock())
|
||||
}
|
||||
|
||||
// TestGetAIProvidersBlocksOnSeedLock asserts that GetAIProviders serializes on
|
||||
// LockIDAIProvidersEnvSeed: while an in-flight seed transaction holds the lock,
|
||||
// the fetch blocks, and once the seed commits the fetch returns the seeded
|
||||
// set. Postgres advisory locks are required, so this cannot run against the
|
||||
// mock store.
|
||||
func TestGetAIProvidersBlocksOnSeedLock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
logger := slogtest.Make(t, nil)
|
||||
|
||||
dbgen.AIProviderWithOptionalKey(t, db, database.AIProvider{
|
||||
Type: database.AIProviderTypeOpenai,
|
||||
Name: "openai",
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.openai.com/",
|
||||
}, "sk-openai")
|
||||
|
||||
srv, err := aibridgedserver.NewServer(ctx, db, logger, "/", codersdk.AIBridgeConfig{}, nil, nil, agplaiseats.Noop{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate an in-flight env seed holding the advisory lock until released.
|
||||
holderReady := make(chan struct{})
|
||||
releaseHolder := make(chan struct{})
|
||||
holderDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(holderDone)
|
||||
txErr := db.InTx(func(tx database.Store) error {
|
||||
if err := tx.AcquireLock(ctx, database.LockIDAIProvidersEnvSeed); err != nil {
|
||||
return err
|
||||
}
|
||||
close(holderReady)
|
||||
<-releaseHolder
|
||||
return nil
|
||||
}, nil)
|
||||
assert.NoError(t, txErr)
|
||||
}()
|
||||
|
||||
testutil.TryReceive(ctx, t, holderReady)
|
||||
|
||||
fetchDone := make(chan *proto.GetAIProvidersResponse, 1)
|
||||
fetchErr := make(chan error, 1)
|
||||
go func() {
|
||||
resp, err := srv.GetAIProviders(ctx, &proto.GetAIProvidersRequest{})
|
||||
fetchErr <- err
|
||||
fetchDone <- resp
|
||||
}()
|
||||
|
||||
// Wait until the fetch goroutine is observably blocked waiting on the seed
|
||||
// advisory lock, rather than inferring it from a fixed delay. AcquireLock
|
||||
// uses the single-bigint advisory lock form, so the waiter appears in
|
||||
// pg_locks as an ungranted "advisory" row whose objid is the low 32 bits of
|
||||
// the lock ID. Asserting the wait directly stops this from passing vacuously
|
||||
// if the goroutine has not yet reached the lock.
|
||||
require.Eventually(t, func() bool {
|
||||
locks, err := db.PGLocks(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, l := range locks {
|
||||
if l.LockType != nil && *l.LockType == "advisory" && !l.Granted &&
|
||||
l.ObjID != nil && *l.ObjID == strconv.Itoa(database.LockIDAIProvidersEnvSeed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, testutil.WaitShort, testutil.IntervalFast, "fetch must block waiting on the seed advisory lock")
|
||||
|
||||
// With the fetch proven to be blocked on the lock, it must not have
|
||||
// completed while the lock is still held.
|
||||
select {
|
||||
case <-fetchDone:
|
||||
t.Fatal("GetAIProviders returned before the seed lock was released")
|
||||
default:
|
||||
}
|
||||
|
||||
// Release the lock; the fetch should now complete and return the seeded set.
|
||||
close(releaseHolder)
|
||||
testutil.TryReceive(ctx, t, holderDone)
|
||||
|
||||
require.NoError(t, testutil.TryReceive(ctx, t, fetchErr))
|
||||
resp := testutil.TryReceive(ctx, t, fetchDone)
|
||||
require.Len(t, resp.GetProviders(), 1)
|
||||
assert.Equal(t, "openai", resp.GetProviders()[0].GetName())
|
||||
assert.Equal(t, []string{"sk-openai"}, resp.GetProviders()[0].GetKeys())
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/aibridged/proto"
|
||||
)
|
||||
|
||||
// Register registers the Recorder, MCPConfigurator, and Authorizer DRPC
|
||||
// services backed by srv onto mux. It is shared by the embedded in-memory
|
||||
// server and the standalone /api/v2/ai-gateway/serve WebSocket handler so both
|
||||
// expose an identical service set.
|
||||
// Register registers the Recorder, MCPConfigurator, Authorizer, and
|
||||
// ProviderConfigurator DRPC services backed by srv onto mux. It is shared by
|
||||
// the embedded in-memory server and the standalone /api/v2/ai-gateway/serve
|
||||
// WebSocket handler so both expose an identical service set.
|
||||
func Register(mux *drpcmux.Mux, srv *Server) error {
|
||||
if err := proto.DRPCRegisterRecorder(mux, srv); err != nil {
|
||||
return xerrors.Errorf("register recorder service: %w", err)
|
||||
@@ -21,5 +21,8 @@ func Register(mux *drpcmux.Mux, srv *Server) error {
|
||||
if err := proto.DRPCRegisterAuthorizer(mux, srv); err != nil {
|
||||
return xerrors.Errorf("register authorizer service: %w", err)
|
||||
}
|
||||
if err := proto.DRPCRegisterProviderConfigurator(mux, srv); err != nil {
|
||||
return xerrors.Errorf("register provider configurator service: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import (
|
||||
"github.com/coder/coder/v2/cli"
|
||||
"github.com/coder/coder/v2/coderd"
|
||||
"github.com/coder/coder/v2/coderd/aibridged"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// StartTestAIBridgeDaemon wires an in-process aibridged daemon onto the
|
||||
@@ -25,6 +23,10 @@ import (
|
||||
// (e.g. chattest.NewOpenAI) will have their requests proxied through the real
|
||||
// aibridged stack as they would in production.
|
||||
//
|
||||
// The daemon starts with an empty pool and fetches providers from coderd over
|
||||
// the in-memory DRPC, then refreshes on ai_providers change events, exactly
|
||||
// like cli.newAIBridgeDaemon.
|
||||
//
|
||||
// metrics is the registry the daemon reports provider reload events to.
|
||||
// The caller owns the metrics instance and can assert on it after the daemon
|
||||
// runs. Use [aibridged.NewMetrics] to create one, or nil for a throwaway.
|
||||
@@ -40,27 +42,16 @@ func StartTestAIBridgeDaemon(
|
||||
cfg := api.DeploymentValues.AI.BridgeConfig
|
||||
tracer := otel.Tracer("aibridge-test")
|
||||
|
||||
providers, _, err := cli.BuildProviders(ctx, api.Database, cfg, logger, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("build providers: %v", err)
|
||||
if metrics == nil {
|
||||
metrics = aibridged.NewMetrics(prometheus.NewRegistry())
|
||||
}
|
||||
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, providers, logger.Named("pool"), nil, tracer)
|
||||
pool, err := aibridged.NewCachedBridgePool(aibridged.DefaultPoolOptions, nil, logger.Named("pool"), nil, tracer)
|
||||
if err != nil {
|
||||
t.Fatalf("create bridge pool: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = pool.Shutdown(context.Background()) })
|
||||
|
||||
if metrics == nil {
|
||||
metrics = aibridged.NewMetrics(prometheus.NewRegistry())
|
||||
}
|
||||
reloader := &testPoolReloader{pool: pool, db: api.Database, cfg: cfg, logger: logger.Named("reloader"), metrics: metrics}
|
||||
unsubscribe, err := aibridged.SubscribeProviderReload(ctx, api.Pubsub, reloader, logger.Named("subscriber"))
|
||||
if err != nil {
|
||||
t.Fatalf("subscribe provider reload: %v", err)
|
||||
}
|
||||
t.Cleanup(unsubscribe)
|
||||
|
||||
srv, err := aibridged.New(ctx, pool, func(dialCtx context.Context) (aibridged.DRPCClient, error) {
|
||||
return api.CreateInMemoryAIBridgeServer(dialCtx)
|
||||
}, logger, tracer)
|
||||
@@ -69,26 +60,14 @@ func StartTestAIBridgeDaemon(
|
||||
}
|
||||
t.Cleanup(func() { _ = srv.Close() })
|
||||
|
||||
// The reloader fetches providers from coderd over srv's DRPC client; the
|
||||
// subscription drives an initial load and refreshes on change events.
|
||||
reloader := cli.NewPoolRPCReloader(pool, srv.Client, cfg, logger.Named("reloader"), nil, metrics)
|
||||
unsubscribe, err := aibridged.SubscribeProviderReload(ctx, api.Pubsub, reloader, logger.Named("subscriber"))
|
||||
if err != nil {
|
||||
t.Fatalf("subscribe provider reload: %v", err)
|
||||
}
|
||||
t.Cleanup(unsubscribe)
|
||||
|
||||
api.RegisterInMemoryAIBridgedHTTPHandler(srv)
|
||||
}
|
||||
|
||||
type testPoolReloader struct {
|
||||
pool *aibridged.CachedBridgePool
|
||||
db database.Store
|
||||
cfg codersdk.AIBridgeConfig
|
||||
logger slog.Logger
|
||||
metrics *aibridged.Metrics
|
||||
}
|
||||
|
||||
func (r *testPoolReloader) Reload(ctx context.Context) error {
|
||||
// Stamp the attempt before building providers so the gap between
|
||||
// attempt and success timestamps reveals a mid-reload hang.
|
||||
r.metrics.RecordReloadAttempt()
|
||||
providers, outcomes, err := cli.BuildProviders(ctx, r.db, r.cfg, r.logger, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.pool.ReplaceProviders(providers)
|
||||
r.metrics.RecordReloadSuccess(outcomes)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -358,6 +358,7 @@ func assertSecurityDefined(t *testing.T, comment SwaggerComment) {
|
||||
authorizedSecurityTags := []string{
|
||||
"CoderSessionToken",
|
||||
"CoderProvisionerKey",
|
||||
"AIGatewayKey",
|
||||
}
|
||||
|
||||
if comment.router == "/api/v2/updatecheck" ||
|
||||
|
||||
Reference in New Issue
Block a user