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:
Danny Kopping
2026-06-29 13:34:58 +02:00
committed by GitHub
parent 74b8f10d4e
commit ce94d42e19
22 changed files with 1288 additions and 408 deletions
+5 -4
View File
@@ -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
}
+4 -2
View File
@@ -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()
+2
View File
@@ -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
}
+468 -112
View File
@@ -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,
+35
View File
@@ -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()
}
+6 -1
View File
@@ -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.
+8 -1
View File
@@ -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))
}
+40
View File
@@ -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
}
+1
View File
@@ -6,4 +6,5 @@ type DRPCServer interface {
proto.DRPCRecorderServer
proto.DRPCMCPConfiguratorServer
proto.DRPCAuthorizerServer
proto.DRPCProviderConfiguratorServer
}
+137
View File
@@ -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 -4
View File
@@ -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
}
+16 -37
View File
@@ -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
}
+1
View File
@@ -358,6 +358,7 @@ func assertSecurityDefined(t *testing.T, comment SwaggerComment) {
authorizedSecurityTags := []string{
"CoderSessionToken",
"CoderProvisionerKey",
"AIGatewayKey",
}
if comment.router == "/api/v2/updatecheck" ||