mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add custom AI provider icons and instance-based model picker grouping (#27026)
This commit is contained in:
@@ -203,6 +203,7 @@ func (api *API) aiProvidersCreate(rw http.ResponseWriter, r *http.Request) {
|
||||
Type: database.AIProviderType(req.Type),
|
||||
Name: req.Name,
|
||||
DisplayName: sql.NullString{String: req.DisplayName, Valid: req.DisplayName != ""},
|
||||
Icon: req.Icon,
|
||||
Enabled: req.Enabled,
|
||||
BaseUrl: req.BaseURL,
|
||||
Settings: settings,
|
||||
@@ -363,6 +364,7 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) {
|
||||
ID: old.ID,
|
||||
Type: old.Type,
|
||||
DisplayName: displayName,
|
||||
Icon: ptr.NilToDefault(req.Icon, old.Icon),
|
||||
Enabled: ptr.NilToDefault(req.Enabled, old.Enabled),
|
||||
BaseUrl: ptr.NilToDefault(req.BaseURL, old.BaseUrl),
|
||||
Settings: settings,
|
||||
|
||||
@@ -44,6 +44,7 @@ func BackfillBedrockProviderType(ctx context.Context, db database.Store, logger
|
||||
ID: provider.ID,
|
||||
Type: database.AIProviderTypeBedrock,
|
||||
DisplayName: provider.DisplayName,
|
||||
Icon: provider.Icon,
|
||||
Enabled: provider.Enabled,
|
||||
BaseUrl: provider.BaseUrl,
|
||||
Settings: provider.Settings,
|
||||
|
||||
@@ -151,6 +151,7 @@ func SeedAIProvidersFromEnv(
|
||||
Type: dp.Type,
|
||||
Name: dp.Name,
|
||||
DisplayName: sql.NullString{String: dp.Name, Valid: true},
|
||||
Icon: "",
|
||||
Enabled: true,
|
||||
BaseUrl: dp.BaseURL,
|
||||
Settings: settings,
|
||||
|
||||
@@ -627,6 +627,7 @@ func TestSeedAIProvidersFromEnv(t *testing.T) {
|
||||
ID: row.ID,
|
||||
Type: database.AIProviderTypeAnthropic,
|
||||
DisplayName: row.DisplayName,
|
||||
Icon: row.Icon,
|
||||
Enabled: row.Enabled,
|
||||
BaseUrl: row.BaseUrl,
|
||||
Settings: row.Settings,
|
||||
|
||||
@@ -90,6 +90,7 @@ func TestAIProvidersCRUD(t *testing.T) {
|
||||
Type: codersdk.AIProviderTypeAnthropic,
|
||||
Name: "primary-anthropic",
|
||||
DisplayName: "Primary Anthropic",
|
||||
Icon: "https://example.com/anthropic.svg",
|
||||
Enabled: true,
|
||||
BaseURL: "https://api.anthropic.com/",
|
||||
Settings: codersdk.AIProviderSettings{
|
||||
@@ -105,6 +106,7 @@ func TestAIProvidersCRUD(t *testing.T) {
|
||||
require.Equal(t, req.Type, created.Type)
|
||||
require.Equal(t, req.Name, created.Name)
|
||||
require.Equal(t, req.DisplayName, created.DisplayName)
|
||||
require.Equal(t, req.Icon, created.Icon)
|
||||
require.Equal(t, req.Enabled, created.Enabled)
|
||||
require.Equal(t, req.BaseURL, created.BaseURL)
|
||||
require.NotNil(t, created.Settings.Bedrock)
|
||||
@@ -128,10 +130,12 @@ func TestAIProvidersCRUD(t *testing.T) {
|
||||
|
||||
// Update.
|
||||
newDisplay := "Updated Display"
|
||||
newIcon := "🦜"
|
||||
newURL := "https://api.anthropic.com/v1"
|
||||
disabled := false
|
||||
updated, err := client.UpdateAIProvider(ctx, created.Name, codersdk.UpdateAIProviderRequest{
|
||||
DisplayName: &newDisplay,
|
||||
Icon: &newIcon,
|
||||
BaseURL: &newURL,
|
||||
Enabled: &disabled,
|
||||
Settings: &codersdk.AIProviderSettings{
|
||||
@@ -143,6 +147,7 @@ func TestAIProvidersCRUD(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, newDisplay, updated.DisplayName)
|
||||
require.Equal(t, newIcon, updated.Icon)
|
||||
require.Equal(t, newURL, updated.BaseURL)
|
||||
require.False(t, updated.Enabled)
|
||||
require.NotNil(t, updated.Settings.Bedrock)
|
||||
|
||||
Generated
+9
@@ -15306,6 +15306,9 @@ const docTemplate = `{
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -18106,6 +18109,9 @@ const docTemplate = `{
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -24842,6 +24848,9 @@ const docTemplate = `{
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/definitions/codersdk.AIProviderSettings"
|
||||
}
|
||||
|
||||
Generated
+9
@@ -13652,6 +13652,9 @@
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -16352,6 +16355,9 @@
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -22815,6 +22821,9 @@
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/definitions/codersdk.AIProviderSettings"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ func AIProvider(row database.AIProvider, keys []database.AIProviderKey) (codersd
|
||||
Type: codersdk.AIProviderType(row.Type),
|
||||
Name: row.Name,
|
||||
DisplayName: display,
|
||||
Icon: row.Icon,
|
||||
Enabled: row.Enabled,
|
||||
BaseURL: row.BaseUrl,
|
||||
APIKeys: maskAIProviderKeys(keys),
|
||||
|
||||
@@ -6848,6 +6848,7 @@ func (s *MethodTestSuite) TestAIBridge() {
|
||||
ID: uuid.New(),
|
||||
Type: database.AIProviderTypeOpenai,
|
||||
Name: "test-provider",
|
||||
Icon: "",
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.example.com/",
|
||||
}
|
||||
@@ -6860,6 +6861,7 @@ func (s *MethodTestSuite) TestAIBridge() {
|
||||
arg := database.UpdateAIProviderParams{
|
||||
ID: provider.ID,
|
||||
Type: provider.Type,
|
||||
Icon: provider.Icon,
|
||||
Enabled: true,
|
||||
BaseUrl: "https://api.example.com/",
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ func AIProvider(t testing.TB, db database.Store, seed database.AIProvider, munge
|
||||
Type: provType,
|
||||
Name: name,
|
||||
DisplayName: displayName,
|
||||
Icon: seed.Icon,
|
||||
Enabled: takeFirst(seed.Enabled, true),
|
||||
// Use an unsupported scheme so leaked test provider calls fail immediately without retries.
|
||||
BaseUrl: takeFirst(seed.BaseUrl, "invalid://test.invalid/"),
|
||||
|
||||
Generated
+1
@@ -1495,6 +1495,7 @@ CREATE TABLE ai_providers (
|
||||
settings_key_id text,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
icon text DEFAULT ''::text NOT NULL,
|
||||
CONSTRAINT ai_providers_name_check CHECK ((name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text))
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE ai_providers
|
||||
DROP COLUMN icon;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE ai_providers
|
||||
ADD COLUMN icon text NOT NULL DEFAULT '';
|
||||
Generated
+1
@@ -4657,6 +4657,7 @@ type AIProvider struct {
|
||||
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"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
Generated
+29
-15
@@ -571,7 +571,7 @@ func (q *sqlQuerier) DeleteAIProviderByID(ctx context.Context, id uuid.UUID) err
|
||||
|
||||
const getAIProviderByID = `-- name: GetAIProviderByID :one
|
||||
SELECT
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
@@ -593,13 +593,14 @@ func (q *sqlQuerier) GetAIProviderByID(ctx context.Context, id uuid.UUID) (AIPro
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Icon,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIProviderByIDForReferenceLock = `-- name: GetAIProviderByIDForReferenceLock :one
|
||||
SELECT
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
@@ -625,13 +626,14 @@ func (q *sqlQuerier) GetAIProviderByIDForReferenceLock(ctx context.Context, id u
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Icon,
|
||||
)
|
||||
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
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
@@ -653,13 +655,14 @@ func (q *sqlQuerier) GetAIProviderByName(ctx context.Context, name string) (AIPr
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Icon,
|
||||
)
|
||||
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
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
|
||||
FROM
|
||||
ai_providers
|
||||
WHERE
|
||||
@@ -697,6 +700,7 @@ func (q *sqlQuerier) GetAIProviders(ctx context.Context, arg GetAIProvidersParam
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Icon,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -717,6 +721,7 @@ INSERT INTO ai_providers (
|
||||
type,
|
||||
name,
|
||||
display_name,
|
||||
icon,
|
||||
enabled,
|
||||
base_url,
|
||||
settings,
|
||||
@@ -726,13 +731,14 @@ INSERT INTO ai_providers (
|
||||
$2::ai_provider_type,
|
||||
$3::text,
|
||||
$4::text,
|
||||
$5::boolean,
|
||||
$6::text,
|
||||
$5::text,
|
||||
$6::boolean,
|
||||
$7::text,
|
||||
$8::text
|
||||
$8::text,
|
||||
$9::text
|
||||
)
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
|
||||
`
|
||||
|
||||
type InsertAIProviderParams struct {
|
||||
@@ -740,6 +746,7 @@ type InsertAIProviderParams struct {
|
||||
Type AIProviderType `db:"type" json:"type"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DisplayName sql.NullString `db:"display_name" json:"display_name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
BaseUrl string `db:"base_url" json:"base_url"`
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
@@ -752,6 +759,7 @@ func (q *sqlQuerier) InsertAIProvider(ctx context.Context, arg InsertAIProviderP
|
||||
arg.Type,
|
||||
arg.Name,
|
||||
arg.DisplayName,
|
||||
arg.Icon,
|
||||
arg.Enabled,
|
||||
arg.BaseUrl,
|
||||
arg.Settings,
|
||||
@@ -770,6 +778,7 @@ func (q *sqlQuerier) InsertAIProvider(ctx context.Context, arg InsertAIProviderP
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Icon,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -780,20 +789,22 @@ UPDATE
|
||||
SET
|
||||
type = $1::ai_provider_type,
|
||||
display_name = $2::text,
|
||||
enabled = $3::boolean,
|
||||
base_url = $4::text,
|
||||
settings = $5::text,
|
||||
settings_key_id = $6::text,
|
||||
icon = $3::text,
|
||||
enabled = $4::boolean,
|
||||
base_url = $5::text,
|
||||
settings = $6::text,
|
||||
settings_key_id = $7::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = $7::uuid AND deleted = FALSE
|
||||
id = $8::uuid AND deleted = FALSE
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
|
||||
`
|
||||
|
||||
type UpdateAIProviderParams struct {
|
||||
Type AIProviderType `db:"type" json:"type"`
|
||||
DisplayName sql.NullString `db:"display_name" json:"display_name"`
|
||||
Icon string `db:"icon" json:"icon"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
BaseUrl string `db:"base_url" json:"base_url"`
|
||||
Settings sql.NullString `db:"settings" json:"settings"`
|
||||
@@ -805,6 +816,7 @@ func (q *sqlQuerier) UpdateAIProvider(ctx context.Context, arg UpdateAIProviderP
|
||||
row := q.db.QueryRowContext(ctx, updateAIProvider,
|
||||
arg.Type,
|
||||
arg.DisplayName,
|
||||
arg.Icon,
|
||||
arg.Enabled,
|
||||
arg.BaseUrl,
|
||||
arg.Settings,
|
||||
@@ -824,6 +836,7 @@ func (q *sqlQuerier) UpdateAIProvider(ctx context.Context, arg UpdateAIProviderP
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Icon,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -838,7 +851,7 @@ SET
|
||||
WHERE
|
||||
id = $3::uuid
|
||||
RETURNING
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at
|
||||
id, type, name, display_name, enabled, deleted, base_url, settings, settings_key_id, created_at, updated_at, icon
|
||||
`
|
||||
|
||||
type UpdateEncryptedAIProviderSettingsParams struct {
|
||||
@@ -866,6 +879,7 @@ func (q *sqlQuerier) UpdateEncryptedAIProviderSettings(ctx context.Context, arg
|
||||
&i.SettingsKeyID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Icon,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ INSERT INTO ai_providers (
|
||||
type,
|
||||
name,
|
||||
display_name,
|
||||
icon,
|
||||
enabled,
|
||||
base_url,
|
||||
settings,
|
||||
@@ -54,6 +55,7 @@ INSERT INTO ai_providers (
|
||||
@type::ai_provider_type,
|
||||
@name::text,
|
||||
sqlc.narg('display_name')::text,
|
||||
@icon::text,
|
||||
@enabled::boolean,
|
||||
@base_url::text,
|
||||
sqlc.narg('settings')::text,
|
||||
@@ -68,6 +70,7 @@ UPDATE
|
||||
SET
|
||||
type = @type::ai_provider_type,
|
||||
display_name = sqlc.narg('display_name')::text,
|
||||
icon = @icon::text,
|
||||
enabled = @enabled::boolean,
|
||||
base_url = @base_url::text,
|
||||
settings = sqlc.narg('settings')::text,
|
||||
|
||||
@@ -6631,6 +6631,7 @@ func convertAIProviderSummary(provider database.AIProvider) codersdk.AIProviderS
|
||||
Type: codersdk.AIProviderType(provider.Type),
|
||||
Name: provider.Name,
|
||||
DisplayName: displayName,
|
||||
Icon: provider.Icon,
|
||||
Enabled: provider.Enabled,
|
||||
Deleted: provider.Deleted,
|
||||
}
|
||||
|
||||
@@ -8718,6 +8718,7 @@ func setOpenAIProviderBaseURL(
|
||||
ID: provider.ID,
|
||||
Type: provider.Type,
|
||||
DisplayName: provider.DisplayName,
|
||||
Icon: provider.Icon,
|
||||
Enabled: provider.Enabled,
|
||||
BaseUrl: baseURL,
|
||||
Settings: provider.Settings,
|
||||
|
||||
Reference in New Issue
Block a user