mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add ai_providers table, queries, dbauthz, audit, RBAC (#24892)
This commit is contained in:
+10
-10
@@ -856,11 +856,11 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
)
|
||||
}
|
||||
|
||||
aibridgeProviders, err := ReadAIBridgeProvidersFromEnv(logger, os.Environ())
|
||||
aiProviders, err := ReadAIProvidersFromEnv(logger, os.Environ())
|
||||
if err != nil {
|
||||
return xerrors.Errorf("read aibridge providers from env: %w", err)
|
||||
return xerrors.Errorf("read AI providers from env: %w", err)
|
||||
}
|
||||
vals.AI.BridgeConfig.Providers = append(vals.AI.BridgeConfig.Providers, aibridgeProviders...)
|
||||
vals.AI.BridgeConfig.Providers = append(vals.AI.BridgeConfig.Providers, aiProviders...)
|
||||
|
||||
// Manage push notifications.
|
||||
webpusher, err := webpush.New(ctx, ptr.Ref(options.Logger.Named("webpush")), options.Database, options.AccessURL.String())
|
||||
@@ -2926,10 +2926,10 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// ReadAIBridgeProvidersFromEnv parses CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
|
||||
// environment variables into a slice of AIBridgeProviderConfig.
|
||||
// ReadAIProvidersFromEnv parses CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
|
||||
// environment variables into a slice of AIProviderConfig.
|
||||
// This follows the same indexed pattern as ReadExternalAuthProvidersFromEnv.
|
||||
func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]codersdk.AIBridgeProviderConfig, error) {
|
||||
func ReadAIProvidersFromEnv(logger slog.Logger, environ []string) ([]codersdk.AIProviderConfig, error) {
|
||||
parsed := serpent.ParseEnviron(environ, "CODER_AIBRIDGE_PROVIDER_")
|
||||
|
||||
// Sort by numeric index so that PROVIDER_2 comes before PROVIDER_10.
|
||||
@@ -2942,7 +2942,7 @@ func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]coder
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
var providers []codersdk.AIBridgeProviderConfig
|
||||
var providers []codersdk.AIProviderConfig
|
||||
for _, v := range parsed {
|
||||
tokens := strings.SplitN(v.Name, "_", 2)
|
||||
if len(tokens) != 2 {
|
||||
@@ -2954,7 +2954,7 @@ func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]coder
|
||||
return nil, xerrors.Errorf("parse number: %s", v.Name)
|
||||
}
|
||||
|
||||
var provider codersdk.AIBridgeProviderConfig
|
||||
var provider codersdk.AIProviderConfig
|
||||
switch {
|
||||
case len(providers) < providerNum:
|
||||
return nil, xerrors.Errorf(
|
||||
@@ -3014,7 +3014,7 @@ func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]coder
|
||||
case "BEDROCK_SMALL_FAST_MODEL":
|
||||
provider.BedrockSmallFastModel = v.Value
|
||||
default:
|
||||
logger.Warn(context.Background(), "ignoring unknown aibridge provider field (check for typos)",
|
||||
logger.Warn(context.Background(), "ignoring unknown AI provider field (check for typos)",
|
||||
slog.F("env", fmt.Sprintf("CODER_AIBRIDGE_PROVIDER_%d_%s", providerNum, key)),
|
||||
)
|
||||
}
|
||||
@@ -3066,7 +3066,7 @@ func ReadAIBridgeProvidersFromEnv(logger slog.Logger, environ []string) ([]coder
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func hasBedrockFields(p codersdk.AIBridgeProviderConfig) bool {
|
||||
func hasBedrockFields(p codersdk.AIProviderConfig) bool {
|
||||
return p.BedrockBaseURL != "" || p.BedrockRegion != "" ||
|
||||
len(p.BedrockAccessKeys) > 0 || len(p.BedrockAccessKeySecrets) > 0 ||
|
||||
p.BedrockModel != "" || p.BedrockSmallFastModel != ""
|
||||
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
func TestReadAIProvidersFromEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
env []string
|
||||
expected []codersdk.AIBridgeProviderConfig
|
||||
expected []codersdk.AIProviderConfig
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
@@ -36,7 +36,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_0_BASE_URL=https://api.anthropic.com/",
|
||||
"CODER_AIBRIDGE_PROVIDER_0_DUMP_DIR=/tmp/aibridge-dump",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{
|
||||
Type: aibridge.ProviderAnthropic,
|
||||
Name: "anthropic-zdr",
|
||||
@@ -55,7 +55,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_1_NAME=anthropic-eu",
|
||||
"CODER_AIBRIDGE_PROVIDER_1_BASE_URL=https://eu.api.anthropic.com/",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderAnthropic, Name: "anthropic-us"},
|
||||
{Type: aibridge.ProviderAnthropic, Name: "anthropic-eu", BaseURL: "https://eu.api.anthropic.com/"},
|
||||
},
|
||||
@@ -65,7 +65,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
env: []string{
|
||||
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI},
|
||||
},
|
||||
},
|
||||
@@ -79,7 +79,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_2_NAME=copilot-custom",
|
||||
"CODER_AIBRIDGE_PROVIDER_2_BASE_URL=https://custom.copilot.com",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderAnthropic, Name: "anthropic-main"},
|
||||
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI},
|
||||
{Type: aibridge.ProviderCopilot, Name: "copilot-custom", BaseURL: "https://custom.copilot.com"},
|
||||
@@ -97,7 +97,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_SMALL_FAST_MODEL=anthropic.claude-3-haiku",
|
||||
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_BASE_URL=https://bedrock.us-west-2.amazonaws.com",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{
|
||||
Type: aibridge.ProviderAnthropic,
|
||||
Name: "anthropic-bedrock",
|
||||
@@ -118,7 +118,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
|
||||
"CODER_AIBRIDGE_PROVIDER_0_NAME=first",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: "first"},
|
||||
{Type: aibridge.ProviderAnthropic, Name: "second"},
|
||||
},
|
||||
@@ -172,7 +172,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_0_KEY=sk-xxx",
|
||||
"SOME_OTHER_VAR=hello",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-xxx"}},
|
||||
},
|
||||
},
|
||||
@@ -186,7 +186,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_ACCESS_KEYS=AKID",
|
||||
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_ACCESS_KEY_SECRETS=secret",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{
|
||||
Type: aibridge.ProviderAnthropic,
|
||||
Name: aibridge.ProviderAnthropic,
|
||||
@@ -245,7 +245,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
|
||||
"CODER_AIBRIDGE_PROVIDER_0_KEYS=sk-a,sk-b,sk-c",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-a", "sk-b", "sk-c"}},
|
||||
},
|
||||
},
|
||||
@@ -255,7 +255,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
|
||||
"CODER_AIBRIDGE_PROVIDER_0_KEYS= sk-a , sk-b ",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-a", "sk-b"}},
|
||||
},
|
||||
},
|
||||
@@ -291,7 +291,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_ACCESS_KEYS=AKID1,AKID2",
|
||||
"CODER_AIBRIDGE_PROVIDER_0_BEDROCK_ACCESS_KEY_SECRETS=secret1,secret2",
|
||||
},
|
||||
expected: []codersdk.AIBridgeProviderConfig{
|
||||
expected: []codersdk.AIProviderConfig{
|
||||
{
|
||||
Type: aibridge.ProviderAnthropic,
|
||||
Name: aibridge.ProviderAnthropic,
|
||||
@@ -324,7 +324,7 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
providers, err := ReadAIBridgeProvidersFromEnv(slogtest.Make(t, nil), tt.env)
|
||||
providers, err := ReadAIProvidersFromEnv(slogtest.Make(t, nil), tt.env)
|
||||
if tt.errContains != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.errContains)
|
||||
@@ -342,20 +342,20 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
// Indices 0, 1, 2, ..., 10 — verifies that 10 sorts after 2,
|
||||
// not between 1 and 2 as a lexicographic sort would do.
|
||||
var env []string
|
||||
var expected []codersdk.AIBridgeProviderConfig
|
||||
var expected []codersdk.AIProviderConfig
|
||||
for i := range 11 {
|
||||
env = append(env,
|
||||
fmt.Sprintf("CODER_AIBRIDGE_PROVIDER_%d_TYPE=openai", i),
|
||||
fmt.Sprintf("CODER_AIBRIDGE_PROVIDER_%d_KEY=sk-%d", i, i),
|
||||
fmt.Sprintf("CODER_AIBRIDGE_PROVIDER_%d_NAME=p%d", i, i),
|
||||
)
|
||||
expected = append(expected, codersdk.AIBridgeProviderConfig{
|
||||
expected = append(expected, codersdk.AIProviderConfig{
|
||||
Type: aibridge.ProviderOpenAI,
|
||||
Name: fmt.Sprintf("p%d", i),
|
||||
Keys: []string{fmt.Sprintf("sk-%d", i)},
|
||||
})
|
||||
}
|
||||
providers, err := ReadAIBridgeProvidersFromEnv(slogtest.Make(t, nil), env)
|
||||
providers, err := ReadAIProvidersFromEnv(slogtest.Make(t, nil), env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, providers)
|
||||
})
|
||||
@@ -365,17 +365,17 @@ func TestReadAIBridgeProvidersFromEnv(t *testing.T) {
|
||||
// A typo like TPYE instead of TYPE should not prevent startup;
|
||||
// the function logs a warning and continues.
|
||||
sink := testutil.NewFakeSink(t)
|
||||
providers, err := ReadAIBridgeProvidersFromEnv(sink.Logger(), []string{
|
||||
providers, err := ReadAIProvidersFromEnv(sink.Logger(), []string{
|
||||
"CODER_AIBRIDGE_PROVIDER_0_TYPE=openai",
|
||||
"CODER_AIBRIDGE_PROVIDER_0_TPYE=openai",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []codersdk.AIBridgeProviderConfig{
|
||||
require.Equal(t, []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI},
|
||||
}, providers)
|
||||
|
||||
warnings := sink.Entries(func(e slog.SinkEntry) bool {
|
||||
return e.Message == "ignoring unknown aibridge provider field (check for typos)"
|
||||
return e.Message == "ignoring unknown AI provider field (check for typos)"
|
||||
})
|
||||
require.Len(t, warnings, 1)
|
||||
require.Len(t, warnings[0].Fields, 1)
|
||||
|
||||
+2
-2
@@ -872,8 +872,8 @@ aibridgeproxy:
|
||||
# (default: <unset>, type: string)
|
||||
key_file: ""
|
||||
# Deprecated: This value is now derived automatically from the configured AI
|
||||
# Bridge providers' base URLs. Setting this value has no effect. This option will
|
||||
# be removed in a future release.
|
||||
# providers' base URLs. Setting this value has no effect. This option will be
|
||||
# removed in a future release.
|
||||
# (default: <unset>, type: string-array)
|
||||
domain_allowlist: []
|
||||
# URL of an upstream HTTP proxy to chain tunneled (non-allowlisted) requests
|
||||
|
||||
Generated
+47
-31
@@ -13889,7 +13889,7 @@ const docTemplate = `{
|
||||
"description": "Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_\u003cN\u003e_\u003cKEY\u003e\nenv vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeProviderConfig"
|
||||
"$ref": "#/definitions/codersdk.AIProviderConfig"
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
@@ -14010,36 +14010,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"description": "BaseURL is the base URL of the upstream provider API.",
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_small_fast_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"dump_dir": {
|
||||
"description": "DumpDir is the directory path for dumping API requests and responses.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeProxyConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14421,6 +14391,36 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"description": "BaseURL is the base URL of the upstream provider API.",
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_small_fast_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"dump_dir": {
|
||||
"description": "DumpDir is the directory path for dumping API requests and responses.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.APIAllowListTarget": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14522,6 +14522,11 @@ const docTemplate = `{
|
||||
"ai_model_price:*",
|
||||
"ai_model_price:read",
|
||||
"ai_model_price:update",
|
||||
"ai_provider:*",
|
||||
"ai_provider:create",
|
||||
"ai_provider:delete",
|
||||
"ai_provider:read",
|
||||
"ai_provider:update",
|
||||
"ai_seat:*",
|
||||
"ai_seat:create",
|
||||
"ai_seat:read",
|
||||
@@ -14737,6 +14742,11 @@ const docTemplate = `{
|
||||
"APIKeyScopeAiModelPriceAll",
|
||||
"APIKeyScopeAiModelPriceRead",
|
||||
"APIKeyScopeAiModelPriceUpdate",
|
||||
"APIKeyScopeAiProviderAll",
|
||||
"APIKeyScopeAiProviderCreate",
|
||||
"APIKeyScopeAiProviderDelete",
|
||||
"APIKeyScopeAiProviderRead",
|
||||
"APIKeyScopeAiProviderUpdate",
|
||||
"APIKeyScopeAiSeatAll",
|
||||
"APIKeyScopeAiSeatCreate",
|
||||
"APIKeyScopeAiSeatRead",
|
||||
@@ -21351,6 +21361,7 @@ const docTemplate = `{
|
||||
"enum": [
|
||||
"*",
|
||||
"ai_model_price",
|
||||
"ai_provider",
|
||||
"ai_seat",
|
||||
"aibridge_interception",
|
||||
"api_key",
|
||||
@@ -21399,6 +21410,7 @@ const docTemplate = `{
|
||||
"x-enum-varnames": [
|
||||
"ResourceWildcard",
|
||||
"ResourceAiModelPrice",
|
||||
"ResourceAIProvider",
|
||||
"ResourceAiSeat",
|
||||
"ResourceAibridgeInterception",
|
||||
"ResourceApiKey",
|
||||
@@ -21658,6 +21670,8 @@ const docTemplate = `{
|
||||
"workspace_app",
|
||||
"task",
|
||||
"ai_seat",
|
||||
"ai_provider",
|
||||
"ai_provider_key",
|
||||
"chat",
|
||||
"user_secret"
|
||||
],
|
||||
@@ -21689,6 +21703,8 @@ const docTemplate = `{
|
||||
"ResourceTypeWorkspaceApp",
|
||||
"ResourceTypeTask",
|
||||
"ResourceTypeAISeat",
|
||||
"ResourceTypeAIProvider",
|
||||
"ResourceTypeAIProviderKey",
|
||||
"ResourceTypeChat",
|
||||
"ResourceTypeUserSecret"
|
||||
]
|
||||
|
||||
Generated
+47
-31
@@ -12361,7 +12361,7 @@
|
||||
"description": "Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_\u003cN\u003e_\u003cKEY\u003e\nenv vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeProviderConfig"
|
||||
"$ref": "#/definitions/codersdk.AIProviderConfig"
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
@@ -12482,36 +12482,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"description": "BaseURL is the base URL of the upstream provider API.",
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_small_fast_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"dump_dir": {
|
||||
"description": "DumpDir is the directory path for dumping API requests and responses.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeProxyConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12893,6 +12863,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIProviderConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"description": "BaseURL is the base URL of the upstream provider API.",
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"bedrock_small_fast_model": {
|
||||
"type": "string"
|
||||
},
|
||||
"dump_dir": {
|
||||
"description": "DumpDir is the directory path for dumping API requests and responses.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the unique instance identifier used for routing.\nDefaults to Type if not provided.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the provider type: \"openai\", \"anthropic\", or \"copilot\".",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.APIAllowListTarget": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12986,6 +12986,11 @@
|
||||
"ai_model_price:*",
|
||||
"ai_model_price:read",
|
||||
"ai_model_price:update",
|
||||
"ai_provider:*",
|
||||
"ai_provider:create",
|
||||
"ai_provider:delete",
|
||||
"ai_provider:read",
|
||||
"ai_provider:update",
|
||||
"ai_seat:*",
|
||||
"ai_seat:create",
|
||||
"ai_seat:read",
|
||||
@@ -13201,6 +13206,11 @@
|
||||
"APIKeyScopeAiModelPriceAll",
|
||||
"APIKeyScopeAiModelPriceRead",
|
||||
"APIKeyScopeAiModelPriceUpdate",
|
||||
"APIKeyScopeAiProviderAll",
|
||||
"APIKeyScopeAiProviderCreate",
|
||||
"APIKeyScopeAiProviderDelete",
|
||||
"APIKeyScopeAiProviderRead",
|
||||
"APIKeyScopeAiProviderUpdate",
|
||||
"APIKeyScopeAiSeatAll",
|
||||
"APIKeyScopeAiSeatCreate",
|
||||
"APIKeyScopeAiSeatRead",
|
||||
@@ -19575,6 +19585,7 @@
|
||||
"enum": [
|
||||
"*",
|
||||
"ai_model_price",
|
||||
"ai_provider",
|
||||
"ai_seat",
|
||||
"aibridge_interception",
|
||||
"api_key",
|
||||
@@ -19623,6 +19634,7 @@
|
||||
"x-enum-varnames": [
|
||||
"ResourceWildcard",
|
||||
"ResourceAiModelPrice",
|
||||
"ResourceAIProvider",
|
||||
"ResourceAiSeat",
|
||||
"ResourceAibridgeInterception",
|
||||
"ResourceApiKey",
|
||||
@@ -19872,6 +19884,8 @@
|
||||
"workspace_app",
|
||||
"task",
|
||||
"ai_seat",
|
||||
"ai_provider",
|
||||
"ai_provider_key",
|
||||
"chat",
|
||||
"user_secret"
|
||||
],
|
||||
@@ -19903,6 +19917,8 @@
|
||||
"ResourceTypeWorkspaceApp",
|
||||
"ResourceTypeTask",
|
||||
"ResourceTypeAISeat",
|
||||
"ResourceTypeAIProvider",
|
||||
"ResourceTypeAIProviderKey",
|
||||
"ResourceTypeChat",
|
||||
"ResourceTypeUserSecret"
|
||||
]
|
||||
|
||||
@@ -34,6 +34,8 @@ type Auditable interface {
|
||||
idpsync.RoleSyncSettings |
|
||||
database.TaskTable |
|
||||
database.AiSeatState |
|
||||
database.AIProvider |
|
||||
database.AIProviderKey |
|
||||
database.Chat |
|
||||
database.UserSecret
|
||||
}
|
||||
|
||||
@@ -134,6 +134,13 @@ func ResourceTarget[T Auditable](tgt T) string {
|
||||
return typed.Name
|
||||
case database.AiSeatState:
|
||||
return "AI Seat"
|
||||
case database.AIProvider:
|
||||
return typed.Name
|
||||
case database.AIProviderKey:
|
||||
// Provider keys have no user-facing name; show the parent
|
||||
// provider's UUID so the row can be correlated back to its
|
||||
// provider in the audit UI.
|
||||
return typed.ProviderID.String()
|
||||
case database.Chat:
|
||||
// Chat titles can contain sensitive content (secrets, internal
|
||||
// project names), so we use a short UUID prefix as a display
|
||||
@@ -210,6 +217,10 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
|
||||
return typed.ID
|
||||
case database.AiSeatState:
|
||||
return typed.UserID
|
||||
case database.AIProvider:
|
||||
return typed.ID
|
||||
case database.AIProviderKey:
|
||||
return typed.ID
|
||||
case database.Chat:
|
||||
return typed.ID
|
||||
case database.UserSecret:
|
||||
@@ -271,6 +282,10 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
|
||||
return database.ResourceTypeTask
|
||||
case database.AiSeatState:
|
||||
return database.ResourceTypeAiSeat
|
||||
case database.AIProvider:
|
||||
return database.ResourceTypeAiProvider
|
||||
case database.AIProviderKey:
|
||||
return database.ResourceTypeAiProviderKey
|
||||
case database.Chat:
|
||||
return database.ResourceTypeChat
|
||||
case database.UserSecret:
|
||||
@@ -335,6 +350,12 @@ func ResourceRequiresOrgID[T Auditable]() bool {
|
||||
return true
|
||||
case database.AiSeatState:
|
||||
return false
|
||||
case database.AIProvider:
|
||||
// AI providers are deployment-scoped, not org-scoped.
|
||||
return false
|
||||
case database.AIProviderKey:
|
||||
// AI provider keys are deployment-scoped, not org-scoped.
|
||||
return false
|
||||
case database.Chat:
|
||||
// Chats always have a non-null organization_id (since
|
||||
// migration 000467).
|
||||
|
||||
@@ -10,6 +10,7 @@ const (
|
||||
CheckAiModelPricesCacheWritePriceCheck CheckConstraint = "ai_model_prices_cache_write_price_check" // ai_model_prices
|
||||
CheckAiModelPricesInputPriceCheck CheckConstraint = "ai_model_prices_input_price_check" // ai_model_prices
|
||||
CheckAiModelPricesOutputPriceCheck CheckConstraint = "ai_model_prices_output_price_check" // ai_model_prices
|
||||
CheckAiProvidersNameCheck CheckConstraint = "ai_providers_name_check" // ai_providers
|
||||
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
|
||||
CheckChatModelConfigsCompressionThresholdCheck CheckConstraint = "chat_model_configs_compression_threshold_check" // chat_model_configs
|
||||
CheckChatModelConfigsContextLimitCheck CheckConstraint = "chat_model_configs_context_limit_check" // chat_model_configs
|
||||
|
||||
@@ -1851,6 +1851,20 @@ func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParam
|
||||
return q.db.CustomRoles(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAIProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteAIProviderByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAIProvider); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteAIProviderKey(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
||||
return deleteQ(q.log, q.auth, q.db.GetAPIKeyByID, q.db.DeleteAPIKeyByID)(ctx, id)
|
||||
}
|
||||
@@ -2488,6 +2502,52 @@ func (q *querier) GetAIModelPriceByProviderModel(ctx context.Context, arg databa
|
||||
return q.db.GetAIModelPriceByProviderModel(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.GetAIProviderByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.GetAIProviderByName(ctx, name)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProviderKey{}, err
|
||||
}
|
||||
return q.db.GetAIProviderKeyByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
|
||||
// This query intentionally returns every key row, including those
|
||||
// whose provider has been soft-deleted, so the dbcrypt key rotation
|
||||
// utility can re-encrypt every row that holds a foreign-key
|
||||
// reference to dbcrypt_keys.
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetAIProviderKeys(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetAIProviderKeysByProviderID(ctx, providerID)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetAIProviders(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
|
||||
}
|
||||
@@ -5193,6 +5253,20 @@ func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.Ins
|
||||
return q.db.InsertAIBridgeUserPrompt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.InsertAIProvider(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProviderKey{}, err
|
||||
}
|
||||
return q.db.InsertAIProviderKey(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
|
||||
// TODO(Cian): ideally this would be encoded in the policy, but system users are just members and we
|
||||
// don't currently have a capability to conditionally deny creating resources by owner ID in a role.
|
||||
@@ -6259,6 +6333,13 @@ func (q *querier) UpdateAIBridgeInterceptionEnded(ctx context.Context, params da
|
||||
return q.db.UpdateAIBridgeInterceptionEnded(ctx, params)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.UpdateAIProvider(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
fetch := func(ctx context.Context, arg database.UpdateAPIKeyByIDParams) (database.APIKey, error) {
|
||||
return q.db.GetAPIKeyByID(ctx, arg.ID)
|
||||
@@ -6545,6 +6626,27 @@ func (q *querier) UpdateCustomRole(ctx context.Context, arg database.UpdateCusto
|
||||
return q.db.UpdateCustomRole(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
// Encrypted columns can be rewritten on any row, including those
|
||||
// whose provider has been soft-deleted, so the dbcrypt rotation can
|
||||
// move every FK reference to a new key digest before old keys are
|
||||
// revoked.
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProviderKey{}, err
|
||||
}
|
||||
return q.db.UpdateEncryptedAIProviderKey(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
|
||||
// Settings can be rewritten on any row, including soft-deleted ones,
|
||||
// so the dbcrypt rotation can move every FK reference to a new key
|
||||
// digest before old keys are revoked.
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
|
||||
return database.AIProvider{}, err
|
||||
}
|
||||
return q.db.UpdateEncryptedAIProviderSettings(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
return q.db.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{UserID: arg.UserID, ProviderID: arg.ProviderID})
|
||||
|
||||
@@ -6208,6 +6208,103 @@ func (s *MethodTestSuite) TestAIBridge() {
|
||||
db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), gomock.Any()).Return(database.AiModelPrice{}, nil).AnyTimes()
|
||||
check.Args(database.GetAIModelPriceByProviderModelParams{}).Asserts(rbac.ResourceAiModelPrice, policy.ActionRead)
|
||||
}))
|
||||
|
||||
s.Run("GetAIProviderByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
dbm.EXPECT().GetAIProviderByID(gomock.Any(), provider.ID).Return(provider, nil).AnyTimes()
|
||||
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(provider)
|
||||
}))
|
||||
s.Run("GetAIProviderByName", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
dbm.EXPECT().GetAIProviderByName(gomock.Any(), provider.Name).Return(provider, nil).AnyTimes()
|
||||
check.Args(provider.Name).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(provider)
|
||||
}))
|
||||
s.Run("GetAIProviders", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
providerA := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
providerB := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
arg := database.GetAIProvidersParams{}
|
||||
dbm.EXPECT().GetAIProviders(gomock.Any(), arg).Return([]database.AIProvider{providerA, providerB}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProvider{providerA, providerB})
|
||||
}))
|
||||
s.Run("InsertAIProvider", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
arg := database.InsertAIProviderParams{
|
||||
ID: uuid.New(),
|
||||
Type: database.AiProviderTypeOpenai,
|
||||
Name: "test-provider",
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.example.com/",
|
||||
}
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{ID: arg.ID, Name: arg.Name})
|
||||
dbm.EXPECT().InsertAIProvider(gomock.Any(), arg).Return(provider, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionCreate).Returns(provider)
|
||||
}))
|
||||
s.Run("UpdateAIProvider", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
arg := database.UpdateAIProviderParams{
|
||||
ID: provider.ID,
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.example.com/",
|
||||
}
|
||||
dbm.EXPECT().UpdateAIProvider(gomock.Any(), arg).Return(provider, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(provider)
|
||||
}))
|
||||
s.Run("DeleteAIProviderByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
dbm.EXPECT().DeleteAIProviderByID(gomock.Any(), provider.ID).Return(nil).AnyTimes()
|
||||
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionDelete).Returns()
|
||||
}))
|
||||
s.Run("UpdateEncryptedAIProviderSettings", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
arg := database.UpdateEncryptedAIProviderSettingsParams{
|
||||
ID: provider.ID,
|
||||
Settings: sql.NullString{String: "encrypted-settings", Valid: true},
|
||||
}
|
||||
dbm.EXPECT().UpdateEncryptedAIProviderSettings(gomock.Any(), arg).Return(provider, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(provider)
|
||||
}))
|
||||
s.Run("GetAIProviderKeyByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
dbm.EXPECT().GetAIProviderKeyByID(gomock.Any(), key.ID).Return(key, nil).AnyTimes()
|
||||
check.Args(key.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns(key)
|
||||
}))
|
||||
s.Run("GetAIProviderKeysByProviderID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
keyA := testutil.Fake(s.T(), faker, database.AIProviderKey{ProviderID: provider.ID})
|
||||
keyB := testutil.Fake(s.T(), faker, database.AIProviderKey{ProviderID: provider.ID})
|
||||
dbm.EXPECT().GetAIProviderKeysByProviderID(gomock.Any(), provider.ID).Return([]database.AIProviderKey{keyA, keyB}, nil).AnyTimes()
|
||||
check.Args(provider.ID).Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProviderKey{keyA, keyB})
|
||||
}))
|
||||
s.Run("GetAIProviderKeys", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
keyA := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
keyB := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
dbm.EXPECT().GetAIProviderKeys(gomock.Any()).Return([]database.AIProviderKey{keyA, keyB}, nil).AnyTimes()
|
||||
check.Args().Asserts(rbac.ResourceAIProvider, policy.ActionRead).Returns([]database.AIProviderKey{keyA, keyB})
|
||||
}))
|
||||
s.Run("InsertAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
arg := database.InsertAIProviderKeyParams{
|
||||
ID: uuid.New(),
|
||||
ProviderID: provider.ID,
|
||||
APIKey: "test-key",
|
||||
}
|
||||
key := testutil.Fake(s.T(), faker, database.AIProviderKey{ID: arg.ID, ProviderID: arg.ProviderID})
|
||||
dbm.EXPECT().InsertAIProviderKey(gomock.Any(), arg).Return(key, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionCreate).Returns(key)
|
||||
}))
|
||||
s.Run("DeleteAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
dbm.EXPECT().DeleteAIProviderKey(gomock.Any(), key.ID).Return(nil).AnyTimes()
|
||||
check.Args(key.ID).Asserts(rbac.ResourceAIProvider, policy.ActionDelete).Returns()
|
||||
}))
|
||||
s.Run("UpdateEncryptedAIProviderKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
key := testutil.Fake(s.T(), faker, database.AIProviderKey{})
|
||||
arg := database.UpdateEncryptedAIProviderKeyParams{
|
||||
ID: key.ID,
|
||||
APIKey: "encrypted-api-key",
|
||||
}
|
||||
dbm.EXPECT().UpdateEncryptedAIProviderKey(gomock.Any(), arg).Return(key, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAIProvider, policy.ActionUpdate).Returns(key)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestTelemetry() {
|
||||
|
||||
@@ -169,6 +169,62 @@ func ChatModelConfig(t testing.TB, db database.Store, seed database.ChatModelCon
|
||||
return cfg
|
||||
}
|
||||
|
||||
func AIProvider(t testing.TB, db database.Store, seed database.AIProvider, munge ...func(*database.InsertAIProviderParams)) database.AIProvider {
|
||||
t.Helper()
|
||||
id := seed.ID
|
||||
if id == uuid.Nil {
|
||||
id = uuid.New()
|
||||
}
|
||||
provType := seed.Type
|
||||
if provType == "" {
|
||||
provType = database.AiProviderTypeOpenai
|
||||
}
|
||||
name := takeFirst(seed.Name, testutil.GetRandomNameHyphenated(t))
|
||||
displayName := seed.DisplayName
|
||||
if !displayName.Valid {
|
||||
displayName = sql.NullString{String: name, Valid: true}
|
||||
}
|
||||
params := database.InsertAIProviderParams{
|
||||
ID: id,
|
||||
Type: provType,
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
Enabled: takeFirst(seed.Enabled, true),
|
||||
BaseUrl: takeFirst(seed.BaseUrl, "https://api.example.com/"),
|
||||
Settings: seed.Settings,
|
||||
SettingsKeyID: seed.SettingsKeyID,
|
||||
}
|
||||
for _, fn := range munge {
|
||||
fn(¶ms)
|
||||
}
|
||||
provider, err := db.InsertAIProvider(genCtx, params)
|
||||
require.NoError(t, err, "insert ai provider")
|
||||
return provider
|
||||
}
|
||||
|
||||
func AIProviderKey(t testing.TB, db database.Store, seed database.AIProviderKey, munge ...func(*database.InsertAIProviderKeyParams)) database.AIProviderKey {
|
||||
t.Helper()
|
||||
id := seed.ID
|
||||
if id == uuid.Nil {
|
||||
id = uuid.New()
|
||||
}
|
||||
now := dbtime.Now()
|
||||
params := database.InsertAIProviderKeyParams{
|
||||
ID: id,
|
||||
ProviderID: seed.ProviderID,
|
||||
APIKey: takeFirst(seed.APIKey, "test-key"),
|
||||
ApiKeyKeyID: seed.ApiKeyKeyID,
|
||||
CreatedAt: takeFirst(seed.CreatedAt, now),
|
||||
UpdatedAt: takeFirst(seed.UpdatedAt, now),
|
||||
}
|
||||
for _, fn := range munge {
|
||||
fn(¶ms)
|
||||
}
|
||||
key, err := db.InsertAIProviderKey(genCtx, params)
|
||||
require.NoError(t, err, "insert ai provider key")
|
||||
return key
|
||||
}
|
||||
|
||||
func ChatProvider(t testing.TB, db database.Store, seed database.ChatProvider, munge ...func(*database.InsertChatProviderParams)) database.ChatProvider {
|
||||
t.Helper()
|
||||
params := database.InsertChatProviderParams{
|
||||
|
||||
@@ -377,6 +377,22 @@ func (m queryMetricsStore) CustomRoles(ctx context.Context, arg database.CustomR
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteAIProviderByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteAIProviderByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAIProviderByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteAIProviderKey(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteAIProviderKey").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteAIProviderKey").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteAPIKeyByID(ctx, id)
|
||||
@@ -985,6 +1001,54 @@ func (m queryMetricsStore) GetAIModelPriceByProviderModel(ctx context.Context, a
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderByName(ctx, name)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderByName").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderByName").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderKeyByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderKeyByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeyByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderKeys(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderKeys").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeys").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviderKeysByProviderID(ctx, providerID)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviderKeysByProviderID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviderKeysByProviderID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIProviders(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetAIProviders").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIProviders").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAPIKeyByID(ctx, id)
|
||||
@@ -3561,6 +3625,22 @@ func (m queryMetricsStore) InsertAIBridgeUserPrompt(ctx context.Context, arg dat
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAIProvider(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertAIProvider").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAIProvider").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAIProviderKey(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertAIProviderKey").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertAIProviderKey").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertAPIKey(ctx, arg)
|
||||
@@ -4513,6 +4593,14 @@ func (m queryMetricsStore) UpdateAIBridgeInterceptionEnded(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateAIProvider(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateAIProvider").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateAIProvider").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateAPIKeyByID(ctx, arg)
|
||||
@@ -4697,6 +4785,22 @@ func (m queryMetricsStore) UpdateCustomRole(ctx context.Context, arg database.Up
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateEncryptedAIProviderKey(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateEncryptedAIProviderKey").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateEncryptedAIProviderKey").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateEncryptedAIProviderSettings(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateEncryptedAIProviderSettings").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateEncryptedAIProviderSettings").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateExternalAuthLink(ctx, arg)
|
||||
|
||||
@@ -603,6 +603,34 @@ func (mr *MockStoreMockRecorder) CustomRoles(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomRoles", reflect.TypeOf((*MockStore)(nil).CustomRoles), ctx, arg)
|
||||
}
|
||||
|
||||
// DeleteAIProviderByID mocks base method.
|
||||
func (m *MockStore) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteAIProviderByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteAIProviderByID indicates an expected call of DeleteAIProviderByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteAIProviderByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAIProviderByID", reflect.TypeOf((*MockStore)(nil).DeleteAIProviderByID), ctx, id)
|
||||
}
|
||||
|
||||
// DeleteAIProviderKey mocks base method.
|
||||
func (m *MockStore) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteAIProviderKey", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteAIProviderKey indicates an expected call of DeleteAIProviderKey.
|
||||
func (mr *MockStoreMockRecorder) DeleteAIProviderKey(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAIProviderKey", reflect.TypeOf((*MockStore)(nil).DeleteAIProviderKey), ctx, id)
|
||||
}
|
||||
|
||||
// DeleteAPIKeyByID mocks base method.
|
||||
func (m *MockStore) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1698,6 +1726,96 @@ func (mr *MockStoreMockRecorder) GetAIModelPriceByProviderModel(ctx, arg any) *g
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIModelPriceByProviderModel", reflect.TypeOf((*MockStore)(nil).GetAIModelPriceByProviderModel), ctx, arg)
|
||||
}
|
||||
|
||||
// GetAIProviderByID mocks base method.
|
||||
func (m *MockStore) GetAIProviderByID(ctx context.Context, id uuid.UUID) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderByID indicates an expected call of GetAIProviderByID.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderByID", reflect.TypeOf((*MockStore)(nil).GetAIProviderByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetAIProviderByName mocks base method.
|
||||
func (m *MockStore) GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderByName", ctx, name)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderByName indicates an expected call of GetAIProviderByName.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderByName(ctx, name any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderByName", reflect.TypeOf((*MockStore)(nil).GetAIProviderByName), ctx, name)
|
||||
}
|
||||
|
||||
// GetAIProviderKeyByID mocks base method.
|
||||
func (m *MockStore) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderKeyByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderKeyByID indicates an expected call of GetAIProviderKeyByID.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderKeyByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeyByID", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeyByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetAIProviderKeys mocks base method.
|
||||
func (m *MockStore) GetAIProviderKeys(ctx context.Context) ([]database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderKeys", ctx)
|
||||
ret0, _ := ret[0].([]database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderKeys indicates an expected call of GetAIProviderKeys.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderKeys(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeys", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeys), ctx)
|
||||
}
|
||||
|
||||
// GetAIProviderKeysByProviderID mocks base method.
|
||||
func (m *MockStore) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviderKeysByProviderID", ctx, providerID)
|
||||
ret0, _ := ret[0].([]database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviderKeysByProviderID indicates an expected call of GetAIProviderKeysByProviderID.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviderKeysByProviderID(ctx, providerID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviderKeysByProviderID", reflect.TypeOf((*MockStore)(nil).GetAIProviderKeysByProviderID), ctx, providerID)
|
||||
}
|
||||
|
||||
// GetAIProviders mocks base method.
|
||||
func (m *MockStore) GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIProviders", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIProviders indicates an expected call of GetAIProviders.
|
||||
func (mr *MockStoreMockRecorder) GetAIProviders(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIProviders", reflect.TypeOf((*MockStore)(nil).GetAIProviders), ctx, arg)
|
||||
}
|
||||
|
||||
// GetAPIKeyByID mocks base method.
|
||||
func (m *MockStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -6677,6 +6795,36 @@ func (mr *MockStoreMockRecorder) InsertAIBridgeUserPrompt(ctx, arg any) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIBridgeUserPrompt", reflect.TypeOf((*MockStore)(nil).InsertAIBridgeUserPrompt), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAIProvider mocks base method.
|
||||
func (m *MockStore) InsertAIProvider(ctx context.Context, arg database.InsertAIProviderParams) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertAIProvider", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertAIProvider indicates an expected call of InsertAIProvider.
|
||||
func (mr *MockStoreMockRecorder) InsertAIProvider(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIProvider", reflect.TypeOf((*MockStore)(nil).InsertAIProvider), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAIProviderKey mocks base method.
|
||||
func (m *MockStore) InsertAIProviderKey(ctx context.Context, arg database.InsertAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertAIProviderKey", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertAIProviderKey indicates an expected call of InsertAIProviderKey.
|
||||
func (mr *MockStoreMockRecorder) InsertAIProviderKey(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAIProviderKey", reflect.TypeOf((*MockStore)(nil).InsertAIProviderKey), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertAPIKey mocks base method.
|
||||
func (m *MockStore) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8539,6 +8687,21 @@ func (mr *MockStoreMockRecorder) UpdateAIBridgeInterceptionEnded(ctx, arg any) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIBridgeInterceptionEnded", reflect.TypeOf((*MockStore)(nil).UpdateAIBridgeInterceptionEnded), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateAIProvider mocks base method.
|
||||
func (m *MockStore) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateAIProvider", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateAIProvider indicates an expected call of UpdateAIProvider.
|
||||
func (mr *MockStoreMockRecorder) UpdateAIProvider(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAIProvider", reflect.TypeOf((*MockStore)(nil).UpdateAIProvider), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateAPIKeyByID mocks base method.
|
||||
func (m *MockStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKeyByIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8881,6 +9044,36 @@ func (mr *MockStoreMockRecorder) UpdateCustomRole(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCustomRole", reflect.TypeOf((*MockStore)(nil).UpdateCustomRole), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateEncryptedAIProviderKey mocks base method.
|
||||
func (m *MockStore) UpdateEncryptedAIProviderKey(ctx context.Context, arg database.UpdateEncryptedAIProviderKeyParams) (database.AIProviderKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateEncryptedAIProviderKey", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProviderKey)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateEncryptedAIProviderKey indicates an expected call of UpdateEncryptedAIProviderKey.
|
||||
func (mr *MockStoreMockRecorder) UpdateEncryptedAIProviderKey(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEncryptedAIProviderKey", reflect.TypeOf((*MockStore)(nil).UpdateEncryptedAIProviderKey), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateEncryptedAIProviderSettings mocks base method.
|
||||
func (m *MockStore) UpdateEncryptedAIProviderSettings(ctx context.Context, arg database.UpdateEncryptedAIProviderSettingsParams) (database.AIProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateEncryptedAIProviderSettings", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AIProvider)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateEncryptedAIProviderSettings indicates an expected call of UpdateEncryptedAIProviderSettings.
|
||||
func (mr *MockStoreMockRecorder) UpdateEncryptedAIProviderSettings(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEncryptedAIProviderSettings", reflect.TypeOf((*MockStore)(nil).UpdateEncryptedAIProviderSettings), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateExternalAuthLink mocks base method.
|
||||
func (m *MockStore) UpdateExternalAuthLink(ctx context.Context, arg database.UpdateExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+75
-2
@@ -10,6 +10,11 @@ CREATE TYPE agent_key_scope_enum AS ENUM (
|
||||
'no_user_data'
|
||||
);
|
||||
|
||||
CREATE TYPE ai_provider_type AS ENUM (
|
||||
'openai',
|
||||
'anthropic'
|
||||
);
|
||||
|
||||
CREATE TYPE ai_seat_usage_reason AS ENUM (
|
||||
'aibridge',
|
||||
'task'
|
||||
@@ -226,7 +231,12 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
'ai_seat:read',
|
||||
'ai_model_price:*',
|
||||
'ai_model_price:read',
|
||||
'ai_model_price:update'
|
||||
'ai_model_price:update',
|
||||
'ai_provider:*',
|
||||
'ai_provider:create',
|
||||
'ai_provider:delete',
|
||||
'ai_provider:read',
|
||||
'ai_provider:update'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
@@ -533,7 +543,9 @@ CREATE TYPE resource_type AS ENUM (
|
||||
'task',
|
||||
'ai_seat',
|
||||
'chat',
|
||||
'user_secret'
|
||||
'user_secret',
|
||||
'ai_provider',
|
||||
'ai_provider_key'
|
||||
);
|
||||
|
||||
CREATE TYPE shareable_workspace_owners AS ENUM (
|
||||
@@ -1108,6 +1120,46 @@ CREATE TABLE ai_model_prices (
|
||||
|
||||
COMMENT ON TABLE ai_model_prices IS 'Per-model token prices used by AI Bridge to compute interception cost.';
|
||||
|
||||
CREATE TABLE ai_provider_keys (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
provider_id uuid NOT NULL,
|
||||
api_key text NOT NULL,
|
||||
api_key_key_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_provider_keys IS 'API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.';
|
||||
|
||||
COMMENT ON COLUMN ai_provider_keys.api_key IS 'API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.';
|
||||
|
||||
COMMENT ON COLUMN ai_provider_keys.api_key_key_id IS 'The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.';
|
||||
|
||||
CREATE TABLE ai_providers (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
type ai_provider_type NOT NULL,
|
||||
name text NOT NULL,
|
||||
display_name text,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
deleted boolean DEFAULT false NOT NULL,
|
||||
base_url text NOT NULL,
|
||||
settings text,
|
||||
settings_key_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT ai_providers_name_check CHECK ((name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_providers IS 'Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.display_name IS 'Optional human-readable label. When NULL, callers should fall back to name.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.deleted IS 'Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.settings IS 'Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.settings_key_id IS 'The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.';
|
||||
|
||||
CREATE TABLE ai_seat_state (
|
||||
user_id uuid NOT NULL,
|
||||
first_used_at timestamp with time zone NOT NULL,
|
||||
@@ -3409,6 +3461,12 @@ ALTER TABLE ONLY workspace_agent_stats
|
||||
ALTER TABLE ONLY ai_model_prices
|
||||
ADD CONSTRAINT ai_model_prices_pkey PRIMARY KEY (provider, model);
|
||||
|
||||
ALTER TABLE ONLY ai_provider_keys
|
||||
ADD CONSTRAINT ai_provider_keys_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY ai_providers
|
||||
ADD CONSTRAINT ai_providers_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY ai_seat_state
|
||||
ADD CONSTRAINT ai_seat_state_pkey PRIMARY KEY (user_id);
|
||||
|
||||
@@ -3775,6 +3833,8 @@ ALTER TABLE ONLY workspace_resources
|
||||
ALTER TABLE ONLY workspaces
|
||||
ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id);
|
||||
|
||||
CREATE UNIQUE INDEX ai_providers_name_unique ON ai_providers USING btree (name) WHERE (deleted = false);
|
||||
|
||||
CREATE INDEX api_keys_last_used_idx ON api_keys USING btree (last_used DESC);
|
||||
|
||||
COMMENT ON INDEX api_keys_last_used_idx IS 'Index for optimizing api_keys queries filtering by last_used';
|
||||
@@ -3783,6 +3843,10 @@ CREATE INDEX idx_agent_stats_created_at ON workspace_agent_stats USING btree (cr
|
||||
|
||||
CREATE INDEX idx_agent_stats_user_id ON workspace_agent_stats USING btree (user_id);
|
||||
|
||||
CREATE INDEX idx_ai_provider_keys_provider_id ON ai_provider_keys USING btree (provider_id);
|
||||
|
||||
CREATE INDEX idx_ai_providers_enabled ON ai_providers USING btree (enabled) WHERE (deleted = false);
|
||||
|
||||
CREATE INDEX idx_aibridge_interceptions_client ON aibridge_interceptions USING btree (client);
|
||||
|
||||
CREATE INDEX idx_aibridge_interceptions_client_session_id ON aibridge_interceptions USING btree (client_session_id) WHERE (client_session_id IS NOT NULL);
|
||||
@@ -4149,6 +4213,15 @@ COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS 'U
|
||||
the uniqueness requirement. A trigger allows us to enforce uniqueness going
|
||||
forward without requiring a migration to clean up historical data.';
|
||||
|
||||
ALTER TABLE ONLY ai_provider_keys
|
||||
ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
|
||||
ALTER TABLE ONLY ai_provider_keys
|
||||
ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ai_providers
|
||||
ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
|
||||
ALTER TABLE ONLY ai_seat_state
|
||||
ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ type ForeignKeyConstraint string
|
||||
|
||||
// ForeignKeyConstraint enums.
|
||||
const (
|
||||
ForeignKeyAiProviderKeysAPIKeyKeyID ForeignKeyConstraint = "ai_provider_keys_api_key_key_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyAiProviderKeysProviderID ForeignKeyConstraint = "ai_provider_keys_provider_id_fkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_provider_id_fkey FOREIGN KEY (provider_id) REFERENCES ai_providers(id) ON DELETE CASCADE;
|
||||
ForeignKeyAiProvidersSettingsKeyID ForeignKeyConstraint = "ai_providers_settings_key_id_fkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_settings_key_id_fkey FOREIGN KEY (settings_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id);
|
||||
ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS ai_provider_keys;
|
||||
DROP TABLE IF EXISTS ai_providers;
|
||||
DROP TYPE IF EXISTS ai_provider_type;
|
||||
-- No-op for ALTER TYPE resource_type / api_key_scope ADD VALUE:
|
||||
-- Postgres does not allow removing enum values safely.
|
||||
@@ -0,0 +1,67 @@
|
||||
CREATE TYPE ai_provider_type AS ENUM (
|
||||
'openai',
|
||||
'anthropic'
|
||||
);
|
||||
|
||||
CREATE TABLE ai_providers (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
type ai_provider_type NOT NULL,
|
||||
name text NOT NULL
|
||||
CONSTRAINT ai_providers_name_check
|
||||
CHECK (name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'),
|
||||
display_name text,
|
||||
enabled boolean NOT NULL DEFAULT TRUE,
|
||||
deleted boolean NOT NULL DEFAULT FALSE,
|
||||
base_url text NOT NULL,
|
||||
settings text,
|
||||
settings_key_id text REFERENCES dbcrypt_keys(active_key_digest),
|
||||
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Provider names are unique among live rows only. Soft-deleted rows
|
||||
-- are retained for audit and FK history but do not reserve names.
|
||||
CREATE UNIQUE INDEX ai_providers_name_unique
|
||||
ON ai_providers (name)
|
||||
WHERE deleted = FALSE;
|
||||
|
||||
COMMENT ON TABLE ai_providers IS 'Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.settings IS 'Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.settings_key_id IS 'The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.deleted IS 'Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.';
|
||||
|
||||
COMMENT ON COLUMN ai_providers.display_name IS 'Optional human-readable label. When NULL, callers should fall back to name.';
|
||||
|
||||
CREATE INDEX idx_ai_providers_enabled ON ai_providers (enabled) WHERE deleted = FALSE;
|
||||
|
||||
CREATE TABLE ai_provider_keys (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider_id uuid NOT NULL REFERENCES ai_providers(id) ON DELETE CASCADE,
|
||||
api_key text NOT NULL,
|
||||
api_key_key_id text REFERENCES dbcrypt_keys(active_key_digest),
|
||||
created_at timestamp with time zone NOT NULL DEFAULT NOW(),
|
||||
updated_at timestamp with time zone NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_provider_keys IS 'API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.';
|
||||
|
||||
COMMENT ON COLUMN ai_provider_keys.api_key IS 'API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.';
|
||||
|
||||
COMMENT ON COLUMN ai_provider_keys.api_key_key_id IS 'The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.';
|
||||
|
||||
CREATE INDEX idx_ai_provider_keys_provider_id ON ai_provider_keys (provider_id);
|
||||
|
||||
-- Audit support: allow ai_providers and ai_provider_keys to appear in
|
||||
-- audit_log.resource_type.
|
||||
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'ai_provider';
|
||||
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'ai_provider_key';
|
||||
|
||||
-- API key scopes for ai_provider resources.
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:*';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_provider:update';
|
||||
@@ -0,0 +1,56 @@
|
||||
INSERT INTO ai_providers (
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
display_name,
|
||||
enabled,
|
||||
deleted,
|
||||
base_url,
|
||||
settings
|
||||
) VALUES
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
|
||||
'openai',
|
||||
'openai',
|
||||
'OpenAI (Fixture)',
|
||||
TRUE,
|
||||
FALSE,
|
||||
'https://api.openai.com/v1/',
|
||||
''
|
||||
),
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a02',
|
||||
'anthropic',
|
||||
'anthropic-bedrock',
|
||||
'Anthropic via Bedrock (Fixture)',
|
||||
TRUE,
|
||||
FALSE,
|
||||
'https://bedrock-runtime.us-west-2.amazonaws.com/',
|
||||
'{"bedrock_region":"us-west-2","bedrock_model":"global.anthropic.claude-sonnet-4-5-20250929-v1:0","bedrock_access_key":"fixture-bedrock-access-key","bedrock_access_key_secret":"fixture-bedrock-access-key-secret"}'
|
||||
),
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a03',
|
||||
'openai',
|
||||
'openai-deleted',
|
||||
'OpenAI (Deleted Fixture)',
|
||||
FALSE,
|
||||
TRUE,
|
||||
'https://api.openai.com/v1/',
|
||||
''
|
||||
);
|
||||
|
||||
INSERT INTO ai_provider_keys (
|
||||
id,
|
||||
provider_id,
|
||||
api_key
|
||||
) VALUES
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1b01',
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
|
||||
'fixture-openai-key'
|
||||
),
|
||||
(
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1b02',
|
||||
'8e3c6e18-2b75-4c3f-9b35-9d1c6f4e1a01',
|
||||
'fixture-openai-key-failover'
|
||||
);
|
||||
+112
-2
@@ -16,6 +16,64 @@ import (
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
)
|
||||
|
||||
type AIProviderType string
|
||||
|
||||
const (
|
||||
AiProviderTypeOpenai AIProviderType = "openai"
|
||||
AiProviderTypeAnthropic AIProviderType = "anthropic"
|
||||
)
|
||||
|
||||
func (e *AIProviderType) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = AIProviderType(s)
|
||||
case string:
|
||||
*e = AIProviderType(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for AIProviderType: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullAIProviderType struct {
|
||||
AIProviderType AIProviderType `json:"ai_provider_type"`
|
||||
Valid bool `json:"valid"` // Valid is true if AIProviderType is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullAIProviderType) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.AIProviderType, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.AIProviderType.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullAIProviderType) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.AIProviderType), nil
|
||||
}
|
||||
|
||||
func (e AIProviderType) Valid() bool {
|
||||
switch e {
|
||||
case AiProviderTypeOpenai,
|
||||
AiProviderTypeAnthropic:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AllAIProviderTypeValues() []AIProviderType {
|
||||
return []AIProviderType{
|
||||
AiProviderTypeOpenai,
|
||||
AiProviderTypeAnthropic,
|
||||
}
|
||||
}
|
||||
|
||||
type APIKeyScope string
|
||||
|
||||
const (
|
||||
@@ -230,6 +288,11 @@ const (
|
||||
ApiKeyScopeAiModelPrice APIKeyScope = "ai_model_price:*"
|
||||
ApiKeyScopeAiModelPriceRead APIKeyScope = "ai_model_price:read"
|
||||
ApiKeyScopeAiModelPriceUpdate APIKeyScope = "ai_model_price:update"
|
||||
ApiKeyScopeAiProvider APIKeyScope = "ai_provider:*"
|
||||
ApiKeyScopeAiProviderCreate APIKeyScope = "ai_provider:create"
|
||||
ApiKeyScopeAiProviderDelete APIKeyScope = "ai_provider:delete"
|
||||
ApiKeyScopeAiProviderRead APIKeyScope = "ai_provider:read"
|
||||
ApiKeyScopeAiProviderUpdate APIKeyScope = "ai_provider:update"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -479,7 +542,12 @@ func (e APIKeyScope) Valid() bool {
|
||||
ApiKeyScopeAiSeatRead,
|
||||
ApiKeyScopeAiModelPrice,
|
||||
ApiKeyScopeAiModelPriceRead,
|
||||
ApiKeyScopeAiModelPriceUpdate:
|
||||
ApiKeyScopeAiModelPriceUpdate,
|
||||
ApiKeyScopeAiProvider,
|
||||
ApiKeyScopeAiProviderCreate,
|
||||
ApiKeyScopeAiProviderDelete,
|
||||
ApiKeyScopeAiProviderRead,
|
||||
ApiKeyScopeAiProviderUpdate:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -698,6 +766,11 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
ApiKeyScopeAiModelPrice,
|
||||
ApiKeyScopeAiModelPriceRead,
|
||||
ApiKeyScopeAiModelPriceUpdate,
|
||||
ApiKeyScopeAiProvider,
|
||||
ApiKeyScopeAiProviderCreate,
|
||||
ApiKeyScopeAiProviderDelete,
|
||||
ApiKeyScopeAiProviderRead,
|
||||
ApiKeyScopeAiProviderUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3225,6 +3298,8 @@ const (
|
||||
ResourceTypeAiSeat ResourceType = "ai_seat"
|
||||
ResourceTypeChat ResourceType = "chat"
|
||||
ResourceTypeUserSecret ResourceType = "user_secret"
|
||||
ResourceTypeAiProvider ResourceType = "ai_provider"
|
||||
ResourceTypeAiProviderKey ResourceType = "ai_provider_key"
|
||||
)
|
||||
|
||||
func (e *ResourceType) Scan(src interface{}) error {
|
||||
@@ -3292,7 +3367,9 @@ func (e ResourceType) Valid() bool {
|
||||
ResourceTypeTask,
|
||||
ResourceTypeAiSeat,
|
||||
ResourceTypeChat,
|
||||
ResourceTypeUserSecret:
|
||||
ResourceTypeUserSecret,
|
||||
ResourceTypeAiProvider,
|
||||
ResourceTypeAiProviderKey:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -3329,6 +3406,8 @@ func AllResourceTypeValues() []ResourceType {
|
||||
ResourceTypeAiSeat,
|
||||
ResourceTypeChat,
|
||||
ResourceTypeUserSecret,
|
||||
ResourceTypeAiProvider,
|
||||
ResourceTypeAiProviderKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4299,6 +4378,37 @@ type AIBridgeUserPrompt struct {
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
// Runtime configuration for AI providers. Authoritative source for the provider set served by aibridged. Replaces deployment-time CODER_AIBRIDGE_* environment variables.
|
||||
type AIProvider struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Type AIProviderType `db:"type" json:"type"`
|
||||
Name string `db:"name" json:"name"`
|
||||
// Optional human-readable label. When NULL, callers should fall back to name.
|
||||
DisplayName sql.NullString `db:"display_name" json:"display_name"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
// Soft delete flag. Soft-deleted rows are preserved for audit and FK history but do not block name reuse by future live rows.
|
||||
Deleted bool `db:"deleted" json:"deleted"`
|
||||
BaseUrl string `db:"base_url" json:"base_url"`
|
||||
// Encrypted JSON blob holding type-specific configuration (e.g. AWS Bedrock region, model, access key secret). Plaintext is a JSON object. NULL when no type-specific settings are required.
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
// The ID of the key used to encrypt settings. If this is NULL, settings is not encrypted.
|
||||
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// API keys associated with AI providers. Bedrock providers have zero keys (they authenticate via settings). OpenAI and Anthropic providers have one or more keys for failover.
|
||||
type AIProviderKey struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
ProviderID uuid.UUID `db:"provider_id" json:"provider_id"`
|
||||
// API key used to authenticate with the upstream AI provider. Encrypted at rest via dbcrypt when api_key_key_id is set.
|
||||
APIKey string `db:"api_key" json:"api_key"`
|
||||
// The ID of the key used to encrypt the provider API key. If this is NULL, the API key is not encrypted.
|
||||
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
// hashed_secret contains a SHA256 hash of the key secret. This is considered a secret and MUST NOT be returned from the API as it is used for API key encryption in app proxying code.
|
||||
|
||||
@@ -99,6 +99,8 @@ type sqlcQuerier interface {
|
||||
CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
|
||||
CreateUserSecret(ctx context.Context, arg CreateUserSecretParams) (UserSecret, error)
|
||||
CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error)
|
||||
DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error
|
||||
DeleteAPIKeyByID(ctx context.Context, id string) error
|
||||
DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteAllChatQueuedMessages(ctx context.Context, chatID uuid.UUID) error
|
||||
@@ -246,6 +248,21 @@ type sqlcQuerier interface {
|
||||
GetAIBridgeToolUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeToolUsage, error)
|
||||
GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeUserPrompt, error)
|
||||
GetAIModelPriceByProviderModel(ctx context.Context, arg GetAIModelPriceByProviderModelParams) (AiModelPrice, error)
|
||||
GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIProvider, error)
|
||||
GetAIProviderByName(ctx context.Context, name string) (AIProvider, error)
|
||||
GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (AIProviderKey, error)
|
||||
// Returns every AI provider key row, including those belonging to a
|
||||
// soft-deleted provider, so the dbcrypt key rotation utility can
|
||||
// re-encrypt their api_key and clear references to retired keys.
|
||||
GetAIProviderKeys(ctx context.Context) ([]AIProviderKey, error)
|
||||
// Returns all keys for a provider, ordered by created_at ASC so the
|
||||
// oldest key is returned first. AI Bridge currently uses the oldest
|
||||
// key per provider; multiple keys are stored to support future
|
||||
// failover and rotation flows.
|
||||
GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]AIProviderKey, error)
|
||||
// Returns AI provider rows. Soft-deleted and disabled rows are excluded
|
||||
// unless include_deleted or include_disabled is set.
|
||||
GetAIProviders(ctx context.Context, arg GetAIProvidersParams) ([]AIProvider, error)
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
// there is no unique constraint on empty token names
|
||||
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
|
||||
@@ -866,6 +883,8 @@ type sqlcQuerier interface {
|
||||
InsertAIBridgeTokenUsage(ctx context.Context, arg InsertAIBridgeTokenUsageParams) (AIBridgeTokenUsage, error)
|
||||
InsertAIBridgeToolUsage(ctx context.Context, arg InsertAIBridgeToolUsageParams) (AIBridgeToolUsage, error)
|
||||
InsertAIBridgeUserPrompt(ctx context.Context, arg InsertAIBridgeUserPromptParams) (AIBridgeUserPrompt, error)
|
||||
InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error)
|
||||
InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error)
|
||||
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
|
||||
// We use the organization_id as the id
|
||||
// for simplicity since all users is
|
||||
@@ -1097,6 +1116,7 @@ type sqlcQuerier interface {
|
||||
UnpinChatByID(ctx context.Context, id uuid.UUID) error
|
||||
UnsetDefaultChatModelConfigs(ctx context.Context) error
|
||||
UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error)
|
||||
UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error)
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateChatBuildAgentBinding(ctx context.Context, arg UpdateChatBuildAgentBindingParams) (Chat, error)
|
||||
UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error)
|
||||
@@ -1159,6 +1179,15 @@ type sqlcQuerier interface {
|
||||
UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateChatWorkspaceBindingParams) (Chat, error)
|
||||
UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error)
|
||||
UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error)
|
||||
// Updates only the encrypted columns (api_key, api_key_key_id) and
|
||||
// the updated_at timestamp on a row. Used by the dbcrypt key
|
||||
// rotation utility to re-encrypt or decrypt rows in place.
|
||||
UpdateEncryptedAIProviderKey(ctx context.Context, arg UpdateEncryptedAIProviderKeyParams) (AIProviderKey, error)
|
||||
// Updates only the encrypted columns (settings, settings_key_id) and
|
||||
// the updated_at timestamp on a row, regardless of its deleted flag.
|
||||
// Used by the dbcrypt key rotation utility to re-encrypt or decrypt
|
||||
// rows in place.
|
||||
UpdateEncryptedAIProviderSettings(ctx context.Context, arg UpdateEncryptedAIProviderSettingsParams) (AIProvider, error)
|
||||
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
|
||||
// Optimistic lock: only update the row if the refresh token in the database
|
||||
// still matches the one we read before attempting the refresh. This prevents
|
||||
|
||||
@@ -111,6 +111,499 @@ func (q *sqlQuerier) ActivityBumpWorkspace(ctx context.Context, arg ActivityBump
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteAIProviderKey = `-- name: DeleteAIProviderKey :exec
|
||||
DELETE FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteAIProviderKey(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAIProviderKey, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAIProviderKeyByID = `-- name: GetAIProviderKeyByID :one
|
||||
SELECT
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
id = $1::uuid
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAIProviderKeyByID(ctx context.Context, id uuid.UUID) (AIProviderKey, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAIProviderKeyByID, id)
|
||||
var i AIProviderKey
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIProviderKeys = `-- name: GetAIProviderKeys :many
|
||||
SELECT
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_provider_keys
|
||||
ORDER BY
|
||||
provider_id ASC,
|
||||
created_at ASC,
|
||||
id ASC
|
||||
`
|
||||
|
||||
// Returns every AI provider key row, including those belonging to a
|
||||
// soft-deleted provider, so the dbcrypt key rotation utility can
|
||||
// re-encrypt their api_key and clear references to retired keys.
|
||||
func (q *sqlQuerier) GetAIProviderKeys(ctx context.Context) ([]AIProviderKey, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAIProviderKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AIProviderKey
|
||||
for rows.Next() {
|
||||
var i AIProviderKey
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getAIProviderKeysByProviderID = `-- name: GetAIProviderKeysByProviderID :many
|
||||
SELECT
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
provider_id = $1::uuid
|
||||
ORDER BY
|
||||
created_at ASC,
|
||||
id ASC
|
||||
`
|
||||
|
||||
// Returns all keys for a provider, ordered by created_at ASC so the
|
||||
// oldest key is returned first. AI Bridge currently uses the oldest
|
||||
// key per provider; multiple keys are stored to support future
|
||||
// failover and rotation flows.
|
||||
func (q *sqlQuerier) GetAIProviderKeysByProviderID(ctx context.Context, providerID uuid.UUID) ([]AIProviderKey, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAIProviderKeysByProviderID, providerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AIProviderKey
|
||||
for rows.Next() {
|
||||
var i AIProviderKey
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertAIProviderKey = `-- name: InsertAIProviderKey :one
|
||||
INSERT INTO ai_provider_keys (
|
||||
id,
|
||||
provider_id,
|
||||
api_key,
|
||||
api_key_key_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::uuid,
|
||||
$3::text,
|
||||
$4::text,
|
||||
$5::timestamptz,
|
||||
$6::timestamptz
|
||||
)
|
||||
RETURNING
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type InsertAIProviderKeyParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
ProviderID uuid.UUID `db:"provider_id" json:"provider_id"`
|
||||
APIKey string `db:"api_key" json:"api_key"`
|
||||
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAIProviderKey(ctx context.Context, arg InsertAIProviderKeyParams) (AIProviderKey, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertAIProviderKey,
|
||||
arg.ID,
|
||||
arg.ProviderID,
|
||||
arg.APIKey,
|
||||
arg.ApiKeyKeyID,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
)
|
||||
var i AIProviderKey
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateEncryptedAIProviderKey = `-- name: UpdateEncryptedAIProviderKey :one
|
||||
UPDATE
|
||||
ai_provider_keys
|
||||
SET
|
||||
api_key = $1::text,
|
||||
api_key_key_id = $2::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $3::uuid
|
||||
RETURNING
|
||||
id, provider_id, api_key, api_key_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateEncryptedAIProviderKeyParams struct {
|
||||
APIKey string `db:"api_key" json:"api_key"`
|
||||
ApiKeyKeyID sql.NullString `db:"api_key_key_id" json:"api_key_key_id"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
// Updates only the encrypted columns (api_key, api_key_key_id) and
|
||||
// the updated_at timestamp on a row. Used by the dbcrypt key
|
||||
// rotation utility to re-encrypt or decrypt rows in place.
|
||||
func (q *sqlQuerier) UpdateEncryptedAIProviderKey(ctx context.Context, arg UpdateEncryptedAIProviderKeyParams) (AIProviderKey, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderKey, arg.APIKey, arg.ApiKeyKeyID, arg.ID)
|
||||
var i AIProviderKey
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProviderID,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteAIProviderByID = `-- name: DeleteAIProviderByID :exec
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
deleted = TRUE,
|
||||
enabled = FALSE,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $1::uuid AND deleted = FALSE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAIProviderByID, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAIProviderByID = `-- name: GetAIProviderByID :one
|
||||
SELECT
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
id = $1::uuid AND deleted = FALSE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAIProviderByID, id)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIProviderByName = `-- name: GetAIProviderByName :one
|
||||
SELECT
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
name = $1::text AND deleted = FALSE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAIProviderByName(ctx context.Context, name string) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAIProviderByName, name)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIProviders = `-- name: GetAIProviders :many
|
||||
SELECT
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
($1::boolean OR NOT deleted)
|
||||
AND ($2::boolean OR enabled)
|
||||
ORDER BY
|
||||
name ASC
|
||||
`
|
||||
|
||||
type GetAIProvidersParams struct {
|
||||
IncludeDeleted bool `db:"include_deleted" json:"include_deleted"`
|
||||
IncludeDisabled bool `db:"include_disabled" json:"include_disabled"`
|
||||
}
|
||||
|
||||
// Returns AI provider rows. Soft-deleted and disabled rows are excluded
|
||||
// unless include_deleted or include_disabled is set.
|
||||
func (q *sqlQuerier) GetAIProviders(ctx context.Context, arg GetAIProvidersParams) ([]AIProvider, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAIProviders, arg.IncludeDeleted, arg.IncludeDisabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []AIProvider
|
||||
for rows.Next() {
|
||||
var i AIProvider
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertAIProvider = `-- name: InsertAIProvider :one
|
||||
INSERT INTO ai_providers (
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
display_name,
|
||||
enabled,
|
||||
base_url,
|
||||
settings,
|
||||
settings_key_id
|
||||
) VALUES (
|
||||
$1::uuid,
|
||||
$2::ai_provider_type,
|
||||
$3::text,
|
||||
$4::text,
|
||||
$5::boolean,
|
||||
$6::text,
|
||||
$7::text,
|
||||
$8::text
|
||||
)
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type InsertAIProviderParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Type AIProviderType `db:"type" json:"type"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName sql.NullString `db:"display_name" json:"display_name"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
BaseUrl string `db:"base_url" json:"base_url"`
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAIProvider(ctx context.Context, arg InsertAIProviderParams) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertAIProvider,
|
||||
arg.ID,
|
||||
arg.Type,
|
||||
arg.Name,
|
||||
arg.DisplayName,
|
||||
arg.Enabled,
|
||||
arg.BaseUrl,
|
||||
arg.Settings,
|
||||
arg.SettingsKeyID,
|
||||
)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateAIProvider = `-- name: UpdateAIProvider :one
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
display_name = $1::text,
|
||||
enabled = $2::boolean,
|
||||
base_url = $3::text,
|
||||
settings = $4::text,
|
||||
settings_key_id = $5::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $6::uuid AND deleted = FALSE
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateAIProviderParams struct {
|
||||
DisplayName sql.NullString `db:"display_name" json:"display_name"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
BaseUrl string `db:"base_url" json:"base_url"`
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateAIProvider(ctx context.Context, arg UpdateAIProviderParams) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateAIProvider,
|
||||
arg.DisplayName,
|
||||
arg.Enabled,
|
||||
arg.BaseUrl,
|
||||
arg.Settings,
|
||||
arg.SettingsKeyID,
|
||||
arg.ID,
|
||||
)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateEncryptedAIProviderSettings = `-- name: UpdateEncryptedAIProviderSettings :one
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
settings = $1::text,
|
||||
settings_key_id = $2::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $3::uuid
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateEncryptedAIProviderSettingsParams struct {
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
SettingsKeyID sql.NullString `db:"settings_key_id" json:"settings_key_id"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
// Updates only the encrypted columns (settings, settings_key_id) and
|
||||
// the updated_at timestamp on a row, regardless of its deleted flag.
|
||||
// Used by the dbcrypt key rotation utility to re-encrypt or decrypt
|
||||
// rows in place.
|
||||
func (q *sqlQuerier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg UpdateEncryptedAIProviderSettingsParams) (AIProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateEncryptedAIProviderSettings, arg.Settings, arg.SettingsKeyID, arg.ID)
|
||||
var i AIProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.DisplayName,
|
||||
&i.Enabled,
|
||||
&i.Deleted,
|
||||
&i.BaseUrl,
|
||||
&i.Settings,
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const calculateAIBridgeInterceptionsTelemetrySummary = `-- name: CalculateAIBridgeInterceptionsTelemetrySummary :one
|
||||
WITH interceptions_in_range AS (
|
||||
-- Get all matching interceptions in the given timeframe.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
-- name: GetAIProviderKeyByID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
id = @id::uuid;
|
||||
|
||||
-- name: GetAIProviderKeysByProviderID :many
|
||||
-- Returns all keys for a provider, ordered by created_at ASC so the
|
||||
-- oldest key is returned first. AI Bridge currently uses the oldest
|
||||
-- key per provider; multiple keys are stored to support future
|
||||
-- failover and rotation flows.
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
provider_id = @provider_id::uuid
|
||||
ORDER BY
|
||||
created_at ASC,
|
||||
id ASC;
|
||||
|
||||
-- name: GetAIProviderKeys :many
|
||||
-- Returns every AI provider key row, including those belonging to a
|
||||
-- soft-deleted provider, so the dbcrypt key rotation utility can
|
||||
-- re-encrypt their api_key and clear references to retired keys.
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_provider_keys
|
||||
ORDER BY
|
||||
provider_id ASC,
|
||||
created_at ASC,
|
||||
id ASC;
|
||||
|
||||
-- name: InsertAIProviderKey :one
|
||||
INSERT INTO ai_provider_keys (
|
||||
id,
|
||||
provider_id,
|
||||
api_key,
|
||||
api_key_key_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@id::uuid,
|
||||
@provider_id::uuid,
|
||||
@api_key::text,
|
||||
sqlc.narg('api_key_key_id')::text,
|
||||
@created_at::timestamptz,
|
||||
@updated_at::timestamptz
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: DeleteAIProviderKey :exec
|
||||
DELETE FROM
|
||||
ai_provider_keys
|
||||
WHERE
|
||||
id = @id::uuid;
|
||||
|
||||
-- name: UpdateEncryptedAIProviderKey :one
|
||||
-- Updates only the encrypted columns (api_key, api_key_key_id) and
|
||||
-- the updated_at timestamp on a row. Used by the dbcrypt key
|
||||
-- rotation utility to re-encrypt or decrypt rows in place.
|
||||
UPDATE
|
||||
ai_provider_keys
|
||||
SET
|
||||
api_key = @api_key::text,
|
||||
api_key_key_id = sqlc.narg('api_key_key_id')::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
@@ -0,0 +1,92 @@
|
||||
-- name: GetAIProviderByID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
id = @id::uuid AND deleted = FALSE;
|
||||
|
||||
-- name: GetAIProviderByName :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
name = @name::text AND deleted = FALSE;
|
||||
|
||||
-- name: GetAIProviders :many
|
||||
-- Returns AI provider rows. Soft-deleted and disabled rows are excluded
|
||||
-- unless include_deleted or include_disabled is set.
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
(@include_deleted::boolean OR NOT deleted)
|
||||
AND (@include_disabled::boolean OR enabled)
|
||||
ORDER BY
|
||||
name ASC;
|
||||
|
||||
-- name: InsertAIProvider :one
|
||||
INSERT INTO ai_providers (
|
||||
id,
|
||||
type,
|
||||
name,
|
||||
display_name,
|
||||
enabled,
|
||||
base_url,
|
||||
settings,
|
||||
settings_key_id
|
||||
) VALUES (
|
||||
@id::uuid,
|
||||
@type::ai_provider_type,
|
||||
@name::text,
|
||||
sqlc.narg('display_name')::text,
|
||||
@enabled::boolean,
|
||||
@base_url::text,
|
||||
sqlc.narg('settings')::text,
|
||||
sqlc.narg('settings_key_id')::text
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateAIProvider :one
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
display_name = sqlc.narg('display_name')::text,
|
||||
enabled = @enabled::boolean,
|
||||
base_url = @base_url::text,
|
||||
settings = sqlc.narg('settings')::text,
|
||||
settings_key_id = sqlc.narg('settings_key_id')::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid AND deleted = FALSE
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: DeleteAIProviderByID :exec
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
deleted = TRUE,
|
||||
enabled = FALSE,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid AND deleted = FALSE;
|
||||
|
||||
-- name: UpdateEncryptedAIProviderSettings :one
|
||||
-- Updates only the encrypted columns (settings, settings_key_id) and
|
||||
-- the updated_at timestamp on a row, regardless of its deleted flag.
|
||||
-- Used by the dbcrypt key rotation utility to re-encrypt or decrypt
|
||||
-- rows in place.
|
||||
UPDATE
|
||||
ai_providers
|
||||
SET
|
||||
settings = sqlc.narg('settings')::text,
|
||||
settings_key_id = sqlc.narg('settings_key_id')::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
@@ -240,6 +240,9 @@ sql:
|
||||
aibridge_token_usage: AIBridgeTokenUsage
|
||||
aibridge_user_prompt: AIBridgeUserPrompt
|
||||
aibridge_model_thought: AIBridgeModelThought
|
||||
ai_provider: AIProvider
|
||||
ai_provider_key: AIProviderKey
|
||||
ai_provider_type: AIProviderType
|
||||
mcp_server_config: MCPServerConfig
|
||||
mcp_server_configs: MCPServerConfigs
|
||||
mcp_server_user_token: MCPServerUserToken
|
||||
|
||||
@@ -8,6 +8,8 @@ type UniqueConstraint string
|
||||
const (
|
||||
UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
|
||||
UniqueAiModelPricesPkey UniqueConstraint = "ai_model_prices_pkey" // ALTER TABLE ONLY ai_model_prices ADD CONSTRAINT ai_model_prices_pkey PRIMARY KEY (provider, model);
|
||||
UniqueAiProviderKeysPkey UniqueConstraint = "ai_provider_keys_pkey" // ALTER TABLE ONLY ai_provider_keys ADD CONSTRAINT ai_provider_keys_pkey PRIMARY KEY (id);
|
||||
UniqueAiProvidersPkey UniqueConstraint = "ai_providers_pkey" // ALTER TABLE ONLY ai_providers ADD CONSTRAINT ai_providers_pkey PRIMARY KEY (id);
|
||||
UniqueAiSeatStatePkey UniqueConstraint = "ai_seat_state_pkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_pkey PRIMARY KEY (user_id);
|
||||
UniqueAibridgeInterceptionsPkey UniqueConstraint = "aibridge_interceptions_pkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_pkey PRIMARY KEY (id);
|
||||
UniqueAibridgeTokenUsagesPkey UniqueConstraint = "aibridge_token_usages_pkey" // ALTER TABLE ONLY aibridge_token_usages ADD CONSTRAINT aibridge_token_usages_pkey PRIMARY KEY (id);
|
||||
@@ -130,6 +132,7 @@ const (
|
||||
UniqueWorkspaceResourceMetadataPkey UniqueConstraint = "workspace_resource_metadata_pkey" // ALTER TABLE ONLY workspace_resource_metadata ADD CONSTRAINT workspace_resource_metadata_pkey PRIMARY KEY (id);
|
||||
UniqueWorkspaceResourcesPkey UniqueConstraint = "workspace_resources_pkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_pkey PRIMARY KEY (id);
|
||||
UniqueWorkspacesPkey UniqueConstraint = "workspaces_pkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id);
|
||||
UniqueAiProvidersNameUnique UniqueConstraint = "ai_providers_name_unique" // CREATE UNIQUE INDEX ai_providers_name_unique ON ai_providers USING btree (name) WHERE (deleted = false);
|
||||
UniqueIndexAPIKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
|
||||
UniqueIndexChatDebugRunsIDChat UniqueConstraint = "idx_chat_debug_runs_id_chat" // CREATE UNIQUE INDEX idx_chat_debug_runs_id_chat ON chat_debug_runs USING btree (id, chat_id);
|
||||
UniqueIndexChatDebugStepsRunStep UniqueConstraint = "idx_chat_debug_steps_run_step" // CREATE UNIQUE INDEX idx_chat_debug_steps_run_step ON chat_debug_steps USING btree (run_id, step_number);
|
||||
|
||||
@@ -23,6 +23,16 @@ var (
|
||||
Type: "ai_model_price",
|
||||
}
|
||||
|
||||
// ResourceAIProvider
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create an AI provider
|
||||
// - "ActionDelete" :: delete an AI provider
|
||||
// - "ActionRead" :: read AI provider configuration
|
||||
// - "ActionUpdate" :: update an AI provider
|
||||
ResourceAIProvider = Object{
|
||||
Type: "ai_provider",
|
||||
}
|
||||
|
||||
// ResourceAiSeat
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: record AI seat usage
|
||||
@@ -450,6 +460,7 @@ func AllResources() []Objecter {
|
||||
return []Objecter{
|
||||
ResourceWildcard,
|
||||
ResourceAiModelPrice,
|
||||
ResourceAIProvider,
|
||||
ResourceAiSeat,
|
||||
ResourceAibridgeInterception,
|
||||
ResourceApiKey,
|
||||
|
||||
@@ -398,6 +398,15 @@ var RBACPermissions = map[string]PermissionDefinition{
|
||||
ActionUpdate: "update AI model prices",
|
||||
},
|
||||
},
|
||||
"ai_provider": {
|
||||
Name: "AIProvider",
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionRead: "read AI provider configuration",
|
||||
ActionCreate: "create an AI provider",
|
||||
ActionUpdate: "update an AI provider",
|
||||
ActionDelete: "delete an AI provider",
|
||||
},
|
||||
},
|
||||
"ai_seat": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: "record AI seat usage",
|
||||
|
||||
@@ -1105,6 +1105,25 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Only owners can manage AI providers. Provider
|
||||
// configuration is deployment-wide and includes secret
|
||||
// material (api_key, settings) so it is not exposed to
|
||||
// org admins or auditors.
|
||||
Name: "AIProviders",
|
||||
Actions: crud,
|
||||
Resource: rbac.ResourceAIProvider,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {
|
||||
memberMe, agentsAccessUser,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
userAdmin, orgUserAdmin, otherOrgUserAdmin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "BoundaryUsage",
|
||||
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
|
||||
@@ -9,6 +9,10 @@ package rbac
|
||||
const (
|
||||
ScopeAiModelPriceRead ScopeName = "ai_model_price:read"
|
||||
ScopeAiModelPriceUpdate ScopeName = "ai_model_price:update"
|
||||
ScopeAiProviderCreate ScopeName = "ai_provider:create"
|
||||
ScopeAiProviderDelete ScopeName = "ai_provider:delete"
|
||||
ScopeAiProviderRead ScopeName = "ai_provider:read"
|
||||
ScopeAiProviderUpdate ScopeName = "ai_provider:update"
|
||||
ScopeAiSeatCreate ScopeName = "ai_seat:create"
|
||||
ScopeAiSeatRead ScopeName = "ai_seat:read"
|
||||
ScopeAibridgeInterceptionCreate ScopeName = "aibridge_interception:create"
|
||||
@@ -177,6 +181,10 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeName("no_user_data"),
|
||||
ScopeAiModelPriceRead,
|
||||
ScopeAiModelPriceUpdate,
|
||||
ScopeAiProviderCreate,
|
||||
ScopeAiProviderDelete,
|
||||
ScopeAiProviderRead,
|
||||
ScopeAiProviderUpdate,
|
||||
ScopeAiSeatCreate,
|
||||
ScopeAiSeatRead,
|
||||
ScopeAibridgeInterceptionCreate,
|
||||
@@ -346,6 +354,10 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeName("no_user_data"),
|
||||
ScopeAiModelPriceRead,
|
||||
ScopeAiModelPriceUpdate,
|
||||
ScopeAiProviderCreate,
|
||||
ScopeAiProviderDelete,
|
||||
ScopeAiProviderRead,
|
||||
ScopeAiProviderUpdate,
|
||||
ScopeAiSeatCreate,
|
||||
ScopeAiSeatRead,
|
||||
ScopeAibridgeInterceptionCreate,
|
||||
|
||||
@@ -9,6 +9,11 @@ const (
|
||||
APIKeyScopeAiModelPriceAll APIKeyScope = "ai_model_price:*"
|
||||
APIKeyScopeAiModelPriceRead APIKeyScope = "ai_model_price:read"
|
||||
APIKeyScopeAiModelPriceUpdate APIKeyScope = "ai_model_price:update"
|
||||
APIKeyScopeAiProviderAll APIKeyScope = "ai_provider:*"
|
||||
APIKeyScopeAiProviderCreate APIKeyScope = "ai_provider:create"
|
||||
APIKeyScopeAiProviderDelete APIKeyScope = "ai_provider:delete"
|
||||
APIKeyScopeAiProviderRead APIKeyScope = "ai_provider:read"
|
||||
APIKeyScopeAiProviderUpdate APIKeyScope = "ai_provider:update"
|
||||
APIKeyScopeAiSeatAll APIKeyScope = "ai_seat:*"
|
||||
APIKeyScopeAiSeatCreate APIKeyScope = "ai_seat:create"
|
||||
APIKeyScopeAiSeatRead APIKeyScope = "ai_seat:read"
|
||||
|
||||
+11
-5
@@ -43,11 +43,13 @@ const (
|
||||
ResourceTypeWorkspaceAgent ResourceType = "workspace_agent"
|
||||
// Deprecated: Workspace App connections are now included in the
|
||||
// connection log.
|
||||
ResourceTypeWorkspaceApp ResourceType = "workspace_app"
|
||||
ResourceTypeTask ResourceType = "task"
|
||||
ResourceTypeAISeat ResourceType = "ai_seat"
|
||||
ResourceTypeChat ResourceType = "chat"
|
||||
ResourceTypeUserSecret ResourceType = "user_secret"
|
||||
ResourceTypeWorkspaceApp ResourceType = "workspace_app"
|
||||
ResourceTypeTask ResourceType = "task"
|
||||
ResourceTypeAISeat ResourceType = "ai_seat"
|
||||
ResourceTypeAIProvider ResourceType = "ai_provider"
|
||||
ResourceTypeAIProviderKey ResourceType = "ai_provider_key"
|
||||
ResourceTypeChat ResourceType = "chat"
|
||||
ResourceTypeUserSecret ResourceType = "user_secret"
|
||||
)
|
||||
|
||||
func (r ResourceType) FriendlyString() string {
|
||||
@@ -108,6 +110,10 @@ func (r ResourceType) FriendlyString() string {
|
||||
return "task"
|
||||
case ResourceTypeAISeat:
|
||||
return "ai seat"
|
||||
case ResourceTypeAIProvider:
|
||||
return "ai provider"
|
||||
case ResourceTypeAIProviderKey:
|
||||
return "ai provider key"
|
||||
case ResourceTypeChat:
|
||||
return "chat"
|
||||
case ResourceTypeUserSecret:
|
||||
|
||||
@@ -4005,7 +4005,7 @@ Write out the current server config as YAML to stdout.`,
|
||||
},
|
||||
{
|
||||
Name: "AI Bridge Proxy Domain Allowlist",
|
||||
Description: "Deprecated: This value is now derived automatically from the configured AI Bridge providers' base URLs. Setting this value has no effect. This option will be removed in a future release.",
|
||||
Description: "Deprecated: This value is now derived automatically from the configured AI providers' base URLs. Setting this value has no effect. This option will be removed in a future release.",
|
||||
Flag: "aibridge-proxy-domain-allowlist",
|
||||
Env: "CODER_AIBRIDGE_PROXY_DOMAIN_ALLOWLIST",
|
||||
Value: &c.AI.BridgeProxyConfig.DomainAllowlist,
|
||||
@@ -4147,7 +4147,7 @@ type AIBridgeConfig struct {
|
||||
LegacyBedrock AIBridgeBedrockConfig `json:"bedrock" typescript:",notnull"`
|
||||
// Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
|
||||
// env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.
|
||||
Providers []AIBridgeProviderConfig `json:"providers,omitempty"`
|
||||
Providers []AIProviderConfig `json:"providers,omitempty"`
|
||||
// Deprecated: Injected MCP in AI Bridge is deprecated and will be removed in a future release.
|
||||
InjectCoderMCPTools serpent.Bool `json:"inject_coder_mcp_tools" typescript:",notnull"`
|
||||
Retention serpent.Duration `json:"retention" typescript:",notnull"`
|
||||
@@ -4187,10 +4187,10 @@ type AIBridgeBedrockConfig struct {
|
||||
SmallFastModel serpent.String `json:"small_fast_model" typescript:",notnull"`
|
||||
}
|
||||
|
||||
// AIBridgeProviderConfig represents a single AI Bridge provider instance,
|
||||
// AIProviderConfig represents a single AI provider instance,
|
||||
// parsed from CODER_AIBRIDGE_PROVIDER_<N>_<KEY> environment variables.
|
||||
// This follows the same indexed pattern as ExternalAuthConfig.
|
||||
type AIBridgeProviderConfig struct {
|
||||
type AIProviderConfig struct {
|
||||
// Type is the provider type: "openai", "anthropic", or "copilot".
|
||||
Type string `json:"type"`
|
||||
// Name is the unique instance identifier used for routing.
|
||||
|
||||
@@ -6,6 +6,7 @@ type RBACResource string
|
||||
const (
|
||||
ResourceWildcard RBACResource = "*"
|
||||
ResourceAiModelPrice RBACResource = "ai_model_price"
|
||||
ResourceAIProvider RBACResource = "ai_provider"
|
||||
ResourceAiSeat RBACResource = "ai_seat"
|
||||
ResourceAibridgeInterception RBACResource = "aibridge_interception"
|
||||
ResourceApiKey RBACResource = "api_key"
|
||||
@@ -80,6 +81,7 @@ const (
|
||||
var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
ResourceWildcard: {},
|
||||
ResourceAiModelPrice: {ActionRead, ActionUpdate},
|
||||
ResourceAIProvider: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceAiSeat: {ActionCreate, ActionRead},
|
||||
ResourceAibridgeInterception: {ActionCreate, ActionRead, ActionUpdate},
|
||||
ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
|
||||
@@ -15,6 +15,8 @@ We track the following resources:
|
||||
|
||||
| <b>Resource<b> | | |
|
||||
|-----------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| AIProvider<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>base_url</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>enabled</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>settings</td><td>true</td></tr><tr><td>settings_key_id</td><td>false</td></tr><tr><td>type</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| AIProviderKey<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>api_key</td><td>true</td></tr><tr><td>api_key_key_id</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>provider_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||
| APIKey<br><i>login, logout, register, create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_list</td><td>false</td></tr><tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>hashed_secret</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>ip_address</td><td>false</td></tr><tr><td>last_used</td><td>true</td></tr><tr><td>lifetime_seconds</td><td>false</td></tr><tr><td>login_type</td><td>false</td></tr><tr><td>scopes</td><td>false</td></tr><tr><td>token_name</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| AiSeatState<br><i>create</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>first_used_at</td><td>true</td></tr><tr><td>last_event_description</td><td>true</td></tr><tr><td>last_event_type</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
| AuditOAuthConvertState<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>expires_at</td><td>true</td></tr><tr><td>from_login_type</td><td>true</td></tr><tr><td>to_login_type</td><td>true</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||
|
||||
Generated
+20
-20
@@ -193,10 +193,10 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
| Property | Value(s) |
|
||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -326,10 +326,10 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
| Property | Value(s) |
|
||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -459,10 +459,10 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
| Property | Value(s) |
|
||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -554,10 +554,10 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
| Property | Value(s) |
|
||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -960,9 +960,9 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
| Property | Value(s) |
|
||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
Generated
+56
-56
File diff suppressed because one or more lines are too long
Generated
+5
-5
@@ -865,11 +865,11 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `type` | `*`, `ai_model_price`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
| `login_type` | `github`, `oidc`, `password`, `token` |
|
||||
| `scope` | `all`, `application_connect` |
|
||||
| Property | Value(s) |
|
||||
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||
| `login_type` | `github`, `oidc`, `password`, `token` |
|
||||
| `scope` | `all`, `application_connect` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ var AuditActionMap = map[string][]codersdk.AuditAction{
|
||||
"License": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
|
||||
"Task": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||
"AiSeatState": {codersdk.AuditActionCreate},
|
||||
"AIProvider": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||
"AIProviderKey": {codersdk.AuditActionCreate, codersdk.AuditActionDelete},
|
||||
"Chat": {codersdk.AuditActionCreate, codersdk.AuditActionWrite}, // chats get 'archived' by users, not deleted.
|
||||
"UserSecret": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||
}
|
||||
@@ -367,6 +369,27 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"last_used_at": ActionIgnore,
|
||||
"updated_at": ActionIgnore,
|
||||
},
|
||||
&database.AIProvider{}: {
|
||||
"id": ActionTrack,
|
||||
"type": ActionTrack,
|
||||
"name": ActionTrack,
|
||||
"display_name": ActionTrack,
|
||||
"enabled": ActionTrack,
|
||||
"deleted": ActionTrack,
|
||||
"base_url": ActionTrack,
|
||||
"settings": ActionSecret, // Encrypted JSON blob may contain provider secrets (e.g. Bedrock access key + secret).
|
||||
"settings_key_id": ActionIgnore, // dbcrypt key reference, derivable.
|
||||
"created_at": ActionIgnore, // Implicit; not useful in a diff.
|
||||
"updated_at": ActionIgnore, // Changes; not useful in a diff.
|
||||
},
|
||||
&database.AIProviderKey{}: {
|
||||
"id": ActionTrack,
|
||||
"provider_id": ActionTrack,
|
||||
"api_key": ActionSecret, // Provider API key, never expose in audit diffs.
|
||||
"api_key_key_id": ActionIgnore, // dbcrypt key reference, derivable.
|
||||
"created_at": ActionIgnore, // Implicit; not useful in a diff.
|
||||
"updated_at": ActionIgnore, // Changes; not useful in a diff.
|
||||
},
|
||||
&database.TaskTable{}: {
|
||||
"id": ActionTrack,
|
||||
"organization_id": ActionIgnore, // Never changes.
|
||||
|
||||
@@ -44,7 +44,7 @@ func newAIBridgeDaemon(coderAPI *coderd.API, providers []aibridge.Provider) (*ai
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// buildProviders constructs the list of aibridge providers from config.
|
||||
// buildProviders constructs the list of AI providers from config.
|
||||
// It merges legacy single-provider env vars and indexed provider configs:
|
||||
// 1. Legacy providers (from CODER_AIBRIDGE_OPENAI_KEY, etc.) are added first.
|
||||
// If a legacy name conflicts with an indexed provider, startup fails with
|
||||
@@ -171,9 +171,9 @@ func buildProviders(cfg codersdk.AIBridgeConfig) ([]aibridge.Provider, error) {
|
||||
}
|
||||
|
||||
// bedrockConfigFromProvider converts Bedrock fields from an indexed
|
||||
// AIBridgeProviderConfig into an aibridge AWSBedrockConfig.
|
||||
// AIProviderConfig into an aibridge AWSBedrockConfig.
|
||||
// Returns nil if no Bedrock fields are set.
|
||||
func bedrockConfigFromProvider(p codersdk.AIBridgeProviderConfig) *aibridge.AWSBedrockConfig {
|
||||
func bedrockConfigFromProvider(p codersdk.AIProviderConfig) *aibridge.AWSBedrockConfig {
|
||||
if p.BedrockRegion == "" && p.BedrockBaseURL == "" && len(p.BedrockAccessKeys) == 0 && len(p.BedrockAccessKeySecrets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestBuildProviders(t *testing.T) {
|
||||
t.Run("IndexedOnly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{
|
||||
Type: aibridge.ProviderAnthropic,
|
||||
Name: "anthropic-zdr",
|
||||
@@ -71,7 +71,7 @@ func TestBuildProviders(t *testing.T) {
|
||||
t.Run("LegacyOpenAIConflictsWithIndexed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: aibridge.ProviderOpenAI, Keys: []string{"sk-indexed"}},
|
||||
},
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func TestBuildProviders(t *testing.T) {
|
||||
t.Run("LegacyAnthropicConflictsWithIndexed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderAnthropic, Name: aibridge.ProviderAnthropic, Keys: []string{"sk-indexed"}},
|
||||
},
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func TestBuildProviders(t *testing.T) {
|
||||
t.Run("MixedLegacyAndIndexed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderAnthropic, Name: "anthropic-zdr", Keys: []string{"sk-zdr"}},
|
||||
},
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func TestBuildProviders(t *testing.T) {
|
||||
t.Run("UnknownType", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: "gemini", Name: "gemini-pro"},
|
||||
},
|
||||
}
|
||||
@@ -166,7 +166,7 @@ func TestBuildProviders(t *testing.T) {
|
||||
// Copilot providers can target any of the three GitHub
|
||||
// Copilot API hosts via an explicit BASE_URL.
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderCopilot, Name: aibridge.ProviderCopilot, DumpDir: "/tmp/copilot-dump"},
|
||||
{Type: aibridge.ProviderCopilot, Name: agplaibridge.ProviderCopilotBusiness, BaseURL: "https://" + agplaibridge.HostCopilotBusiness},
|
||||
{Type: aibridge.ProviderCopilot, Name: agplaibridge.ProviderCopilotEnterprise, BaseURL: "https://" + agplaibridge.HostCopilotEnterprise},
|
||||
@@ -190,7 +190,7 @@ func TestBuildProviders(t *testing.T) {
|
||||
// ChatGPT is an OpenAI-compatible provider with a custom
|
||||
// base URL. Admins configure it as an indexed openai provider.
|
||||
cfg := codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: agplaibridge.ProviderChatGPT, BaseURL: agplaibridge.BaseURLChatGPT},
|
||||
},
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func TestDomainsFromProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
providers, err := buildProviders(codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: "openai", Keys: []string{"k"}},
|
||||
{Type: aibridge.ProviderAnthropic, Name: "anthropic", Keys: []string{"k"}},
|
||||
{Type: aibridge.ProviderOpenAI, Name: "custom", Keys: []string{"k"}, BaseURL: "https://custom-llm.example.com:8443/api"},
|
||||
@@ -243,7 +243,7 @@ func TestDomainsFromProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
providers, err := buildProviders(codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: "first", Keys: []string{"k"}, BaseURL: "https://api.example.com/v1"},
|
||||
{Type: aibridge.ProviderOpenAI, Name: "second", Keys: []string{"k"}, BaseURL: "https://api.example.com/v2"},
|
||||
},
|
||||
@@ -268,7 +268,7 @@ func TestDomainsFromProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
providers, err := buildProviders(codersdk.AIBridgeConfig{
|
||||
Providers: []codersdk.AIBridgeProviderConfig{
|
||||
Providers: []codersdk.AIProviderConfig{
|
||||
{Type: aibridge.ProviderOpenAI, Name: "provider", Keys: []string{"k"}, BaseURL: "https://API.Example.COM/v1"},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -168,7 +168,7 @@ func (r *RootCmd) Server(_ func()) *serpent.Command {
|
||||
if bridgeEnabled || proxyEnabled {
|
||||
providers, err := buildProviders(options.DeploymentValues.AI.BridgeConfig)
|
||||
if err != nil {
|
||||
return nil, nil, xerrors.Errorf("build aibridge providers: %w", err)
|
||||
return nil, nil, xerrors.Errorf("build AI providers: %w", err)
|
||||
}
|
||||
|
||||
// In-memory aibridge daemon.
|
||||
|
||||
@@ -12,6 +12,12 @@ export const RBACResourceActions: Partial<
|
||||
read: "read AI model prices",
|
||||
update: "update AI model prices",
|
||||
},
|
||||
ai_provider: {
|
||||
create: "create an AI provider",
|
||||
delete: "delete an AI provider",
|
||||
read: "read AI provider configuration",
|
||||
update: "update an AI provider",
|
||||
},
|
||||
ai_seat: {
|
||||
create: "record AI seat usage",
|
||||
read: "read AI seat state",
|
||||
|
||||
Generated
+46
-30
@@ -57,7 +57,7 @@ export interface AIBridgeConfig {
|
||||
* Providers holds provider instances populated from CODER_AIBRIDGE_PROVIDER_<N>_<KEY>
|
||||
* env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.
|
||||
*/
|
||||
readonly providers?: readonly AIBridgeProviderConfig[];
|
||||
readonly providers?: readonly AIProviderConfig[];
|
||||
/**
|
||||
* @deprecated Injected MCP in AI Bridge is deprecated and will be removed in a future release.
|
||||
*/
|
||||
@@ -129,35 +129,6 @@ export interface AIBridgeOpenAIConfig {
|
||||
readonly key: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
/**
|
||||
* AIBridgeProviderConfig represents a single AI Bridge provider instance,
|
||||
* parsed from CODER_AIBRIDGE_PROVIDER_<N>_<KEY> environment variables.
|
||||
* This follows the same indexed pattern as ExternalAuthConfig.
|
||||
*/
|
||||
export interface AIBridgeProviderConfig {
|
||||
/**
|
||||
* Type is the provider type: "openai", "anthropic", or "copilot".
|
||||
*/
|
||||
readonly type: string;
|
||||
/**
|
||||
* Name is the unique instance identifier used for routing.
|
||||
* Defaults to Type if not provided.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* BaseURL is the base URL of the upstream provider API.
|
||||
*/
|
||||
readonly base_url: string;
|
||||
/**
|
||||
* DumpDir is the directory path for dumping API requests and responses.
|
||||
*/
|
||||
readonly dump_dir?: string;
|
||||
readonly bedrock_region?: string;
|
||||
readonly bedrock_model?: string;
|
||||
readonly bedrock_small_fast_model?: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface AIBridgeProxyConfig {
|
||||
readonly enabled: boolean;
|
||||
@@ -327,6 +298,35 @@ export interface AIConfig {
|
||||
readonly chat?: ChatConfig;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
/**
|
||||
* AIProviderConfig represents a single AI provider instance,
|
||||
* parsed from CODER_AIBRIDGE_PROVIDER_<N>_<KEY> environment variables.
|
||||
* This follows the same indexed pattern as ExternalAuthConfig.
|
||||
*/
|
||||
export interface AIProviderConfig {
|
||||
/**
|
||||
* Type is the provider type: "openai", "anthropic", or "copilot".
|
||||
*/
|
||||
readonly type: string;
|
||||
/**
|
||||
* Name is the unique instance identifier used for routing.
|
||||
* Defaults to Type if not provided.
|
||||
*/
|
||||
readonly name: string;
|
||||
/**
|
||||
* BaseURL is the base URL of the upstream provider API.
|
||||
*/
|
||||
readonly base_url: string;
|
||||
/**
|
||||
* DumpDir is the directory path for dumping API requests and responses.
|
||||
*/
|
||||
readonly dump_dir?: string;
|
||||
readonly bedrock_region?: string;
|
||||
readonly bedrock_model?: string;
|
||||
readonly bedrock_small_fast_model?: string;
|
||||
}
|
||||
|
||||
// From codersdk/allowlist.go
|
||||
/**
|
||||
* APIAllowListTarget represents a single allow-list entry using the canonical
|
||||
@@ -362,6 +362,11 @@ export type APIKeyScope =
|
||||
| "ai_model_price:*"
|
||||
| "ai_model_price:read"
|
||||
| "ai_model_price:update"
|
||||
| "ai_provider:*"
|
||||
| "ai_provider:create"
|
||||
| "ai_provider:delete"
|
||||
| "ai_provider:read"
|
||||
| "ai_provider:update"
|
||||
| "ai_seat:*"
|
||||
| "ai_seat:create"
|
||||
| "ai_seat:read"
|
||||
@@ -577,6 +582,11 @@ export const APIKeyScopes: APIKeyScope[] = [
|
||||
"ai_model_price:*",
|
||||
"ai_model_price:read",
|
||||
"ai_model_price:update",
|
||||
"ai_provider:*",
|
||||
"ai_provider:create",
|
||||
"ai_provider:delete",
|
||||
"ai_provider:read",
|
||||
"ai_provider:update",
|
||||
"ai_seat:*",
|
||||
"ai_seat:create",
|
||||
"ai_seat:read",
|
||||
@@ -6432,6 +6442,7 @@ export const RBACActions: RBACAction[] = [
|
||||
|
||||
// From codersdk/rbacresources_gen.go
|
||||
export type RBACResource =
|
||||
| "ai_provider"
|
||||
| "ai_model_price"
|
||||
| "ai_seat"
|
||||
| "aibridge_interception"
|
||||
@@ -6480,6 +6491,7 @@ export type RBACResource =
|
||||
| "workspace_proxy";
|
||||
|
||||
export const RBACResources: RBACResource[] = [
|
||||
"ai_provider",
|
||||
"ai_model_price",
|
||||
"ai_seat",
|
||||
"aibridge_interception",
|
||||
@@ -6639,6 +6651,8 @@ export interface ResolveAutostartResponse {
|
||||
|
||||
// From codersdk/audit.go
|
||||
export type ResourceType =
|
||||
| "ai_provider"
|
||||
| "ai_provider_key"
|
||||
| "ai_seat"
|
||||
| "api_key"
|
||||
| "chat"
|
||||
@@ -6670,6 +6684,8 @@ export type ResourceType =
|
||||
| "workspace_proxy";
|
||||
|
||||
export const ResourceTypes: ResourceType[] = [
|
||||
"ai_provider",
|
||||
"ai_provider_key",
|
||||
"ai_seat",
|
||||
"api_key",
|
||||
"chat",
|
||||
|
||||
Reference in New Issue
Block a user