mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08: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
|
||||
|
||||
Reference in New Issue
Block a user