mirror of
https://github.com/coder/coder.git
synced 2026-09-01 14:53:15 +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,
|
||||
|
||||
@@ -195,6 +195,7 @@ type AIProvider struct {
|
||||
Type AIProviderType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Icon string `json:"icon"`
|
||||
Enabled bool `json:"enabled"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKeys []AIProviderKey `json:"api_keys"`
|
||||
@@ -222,6 +223,7 @@ type CreateAIProviderRequest struct {
|
||||
Type AIProviderType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKeys []string `json:"api_keys,omitempty"`
|
||||
@@ -300,6 +302,7 @@ func (req CreateAIProviderRequest) Validate() []ValidationError {
|
||||
// clears all keys.
|
||||
type UpdateAIProviderRequest struct {
|
||||
DisplayName *string `json:"display_name,omitempty"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
BaseURL *string `json:"base_url,omitempty"`
|
||||
APIKeys *[]AIProviderKeyMutation `json:"api_keys,omitempty"`
|
||||
@@ -338,7 +341,7 @@ func (req UpdateAIProviderRequest) Validate() []ValidationError {
|
||||
|
||||
// IsEmpty reports whether the patch carries no fields.
|
||||
func (req UpdateAIProviderRequest) IsEmpty() bool {
|
||||
return req.DisplayName == nil && req.Enabled == nil && req.BaseURL == nil && req.APIKeys == nil && req.Settings == nil
|
||||
return req.DisplayName == nil && req.Icon == nil && req.Enabled == nil && req.BaseURL == nil && req.APIKeys == nil && req.Settings == nil
|
||||
}
|
||||
|
||||
func validateAIProviderName(name string) []ValidationError {
|
||||
|
||||
@@ -1170,6 +1170,7 @@ type ChatProviderConfig struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
Provider string `json:"provider"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Icon string `json:"icon"`
|
||||
Enabled bool `json:"enabled"`
|
||||
HasAPIKey bool `json:"has_api_key"`
|
||||
CentralAPIKeyEnabled bool `json:"central_api_key_enabled"`
|
||||
@@ -1185,6 +1186,7 @@ type ChatProviderConfig struct {
|
||||
type CreateChatProviderConfigRequest struct {
|
||||
Provider string `json:"provider"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
BaseURL string `json:"base_url,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
@@ -1196,6 +1198,7 @@ type CreateChatProviderConfigRequest struct {
|
||||
// UpdateChatProviderConfigRequest updates a chat provider config.
|
||||
type UpdateChatProviderConfigRequest struct {
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
APIKey *string `json:"api_key,omitempty"`
|
||||
BaseURL *string `json:"base_url,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
@@ -1210,6 +1213,7 @@ type AIProviderSummary struct {
|
||||
Type AIProviderType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Icon string `json:"icon"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
@@ -1235,6 +1239,7 @@ type UserChatProviderConfig struct {
|
||||
ProviderID uuid.UUID `json:"provider_id" format:"uuid"`
|
||||
Provider string `json:"provider"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Icon string `json:"icon"`
|
||||
HasUserAPIKey bool `json:"has_user_api_key"`
|
||||
HasCentralAPIKeyFallback bool `json:"has_central_api_key_fallback"`
|
||||
BYOKEnabled bool `json:"byok_enabled"`
|
||||
|
||||
@@ -16,7 +16,7 @@ We track the following resources:
|
||||
| <b>Resource<b> | | |
|
||||
|-----------------------------------------------------------------|----------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| AIGatewayKey<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>hashed_secret</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_heartbeat_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>secret_prefix</td><td>true</td></tr></tbody></table> |
|
||||
| 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> |
|
||||
| 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>icon</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> |
|
||||
| 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> |
|
||||
| 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> |
|
||||
|
||||
Generated
+7
@@ -31,6 +31,7 @@ curl -X GET http://coder-server:8080/api/v2/ai/providers \
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"settings": {},
|
||||
@@ -61,6 +62,7 @@ Status Code **200**
|
||||
| `» created_at` | string(date-time) | false | | |
|
||||
| `» display_name` | string | false | | |
|
||||
| `» enabled` | boolean | false | | |
|
||||
| `» icon` | string | false | | |
|
||||
| `» id` | string(uuid) | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» settings` | [codersdk.AIProviderSettings](schemas.md#codersdkaiprovidersettings) | false | | |
|
||||
@@ -99,6 +101,7 @@ curl -X POST http://coder-server:8080/api/v2/ai/providers \
|
||||
"base_url": "string",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"name": "string",
|
||||
"settings": {},
|
||||
"type": "openai"
|
||||
@@ -128,6 +131,7 @@ curl -X POST http://coder-server:8080/api/v2/ai/providers \
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"settings": {},
|
||||
@@ -180,6 +184,7 @@ curl -X GET http://coder-server:8080/api/v2/ai/providers/{idOrName} \
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"settings": {},
|
||||
@@ -249,6 +254,7 @@ curl -X PATCH http://coder-server:8080/api/v2/ai/providers/{idOrName} \
|
||||
"base_url": "string",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"settings": {}
|
||||
}
|
||||
```
|
||||
@@ -277,6 +283,7 @@ curl -X PATCH http://coder-server:8080/api/v2/ai/providers/{idOrName} \
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"settings": {},
|
||||
|
||||
Generated
+6
@@ -1020,6 +1020,7 @@
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"settings": {},
|
||||
@@ -1037,6 +1038,7 @@
|
||||
| `created_at` | string | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `icon` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `settings` | [codersdk.AIProviderSettings](#codersdkaiprovidersettings) | false | | |
|
||||
@@ -4327,6 +4329,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
"base_url": "string",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"name": "string",
|
||||
"settings": {},
|
||||
"type": "openai"
|
||||
@@ -4341,6 +4344,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `base_url` | string | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `icon` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `settings` | [codersdk.AIProviderSettings](#codersdkaiprovidersettings) | false | | |
|
||||
| `type` | [codersdk.AIProviderType](#codersdkaiprovidertype) | false | | |
|
||||
@@ -13090,6 +13094,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
"base_url": "string",
|
||||
"display_name": "string",
|
||||
"enabled": true,
|
||||
"icon": "string",
|
||||
"settings": {}
|
||||
}
|
||||
```
|
||||
@@ -13102,6 +13107,7 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
| `base_url` | string | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `enabled` | boolean | false | | |
|
||||
| `icon` | string | false | | |
|
||||
| `settings` | [codersdk.AIProviderSettings](#codersdkaiprovidersettings) | false | | |
|
||||
|
||||
## codersdk.UpdateActiveTemplateVersion
|
||||
|
||||
@@ -400,6 +400,7 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"type": ActionTrack,
|
||||
"name": ActionTrack,
|
||||
"display_name": ActionTrack,
|
||||
"icon": ActionTrack,
|
||||
"enabled": ActionTrack,
|
||||
"deleted": ActionTrack,
|
||||
"base_url": ActionTrack,
|
||||
|
||||
@@ -1197,6 +1197,7 @@ func TestAIProviders(t *testing.T) {
|
||||
ID: provider.ID,
|
||||
Type: provider.Type,
|
||||
DisplayName: provider.DisplayName,
|
||||
Icon: provider.Icon,
|
||||
Enabled: provider.Enabled,
|
||||
BaseUrl: provider.BaseUrl,
|
||||
Settings: sql.NullString{String: newSettings, Valid: true},
|
||||
@@ -1214,6 +1215,7 @@ func TestAIProviders(t *testing.T) {
|
||||
ID: provider.ID,
|
||||
Type: provider.Type,
|
||||
DisplayName: provider.DisplayName,
|
||||
Icon: provider.Icon,
|
||||
Enabled: provider.Enabled,
|
||||
BaseUrl: provider.BaseUrl,
|
||||
Settings: sql.NullString{},
|
||||
|
||||
@@ -1785,6 +1785,7 @@ const toChatProviderConfig = (
|
||||
id: provider.id,
|
||||
provider: provider.type,
|
||||
display_name: provider.display_name || provider.type,
|
||||
icon: provider.icon,
|
||||
enabled: provider.enabled,
|
||||
has_api_key: provider.api_keys.length > 0,
|
||||
central_api_key_enabled: true,
|
||||
@@ -1824,6 +1825,7 @@ export const userChatProviderConfigs = () => ({
|
||||
provider_id: config.provider.id,
|
||||
provider: config.provider.type,
|
||||
display_name: config.provider.display_name || config.provider.type,
|
||||
icon: config.provider.icon,
|
||||
has_user_api_key: config.has_user_api_key,
|
||||
byok_enabled: config.byok_enabled,
|
||||
has_central_api_key_fallback: config.has_provider_api_key,
|
||||
|
||||
Generated
+8
@@ -285,6 +285,7 @@ export interface AIProvider {
|
||||
readonly type: AIProviderType;
|
||||
readonly name: string;
|
||||
readonly display_name: string;
|
||||
readonly icon: string;
|
||||
readonly enabled: boolean;
|
||||
readonly base_url: string;
|
||||
readonly api_keys: readonly AIProviderKey[];
|
||||
@@ -440,6 +441,7 @@ export interface AIProviderSummary {
|
||||
readonly type: AIProviderType;
|
||||
readonly name: string;
|
||||
readonly display_name: string;
|
||||
readonly icon: string;
|
||||
readonly enabled: boolean;
|
||||
readonly deleted: boolean;
|
||||
}
|
||||
@@ -2826,6 +2828,7 @@ export interface ChatProviderConfig {
|
||||
readonly id: string;
|
||||
readonly provider: string;
|
||||
readonly display_name: string;
|
||||
readonly icon: string;
|
||||
readonly enabled: boolean;
|
||||
readonly has_api_key: boolean;
|
||||
readonly central_api_key_enabled: boolean;
|
||||
@@ -3489,6 +3492,7 @@ export interface CreateAIProviderRequest {
|
||||
readonly type: AIProviderType;
|
||||
readonly name: string;
|
||||
readonly display_name?: string;
|
||||
readonly icon?: string;
|
||||
readonly enabled: boolean;
|
||||
readonly base_url: string;
|
||||
readonly api_keys?: readonly string[];
|
||||
@@ -3544,6 +3548,7 @@ export interface CreateChatModelConfigRequest {
|
||||
export interface CreateChatProviderConfigRequest {
|
||||
readonly provider: string;
|
||||
readonly display_name?: string;
|
||||
readonly icon?: string;
|
||||
readonly api_key?: string;
|
||||
readonly base_url?: string;
|
||||
readonly enabled?: boolean;
|
||||
@@ -8826,6 +8831,7 @@ export interface TransitionStats {
|
||||
*/
|
||||
export interface UpdateAIProviderRequest {
|
||||
readonly display_name?: string;
|
||||
readonly icon?: string;
|
||||
readonly enabled?: boolean;
|
||||
readonly base_url?: string;
|
||||
readonly api_keys?: AIProviderKeyMutation[];
|
||||
@@ -8971,6 +8977,7 @@ export interface UpdateChatPlanModeInstructionsRequest {
|
||||
*/
|
||||
export interface UpdateChatProviderConfigRequest {
|
||||
readonly display_name?: string;
|
||||
readonly icon?: string;
|
||||
readonly api_key?: string;
|
||||
readonly base_url?: string;
|
||||
readonly enabled?: boolean;
|
||||
@@ -9723,6 +9730,7 @@ export interface UserChatProviderConfig {
|
||||
readonly provider_id: string;
|
||||
readonly provider: string;
|
||||
readonly display_name: string;
|
||||
readonly icon: string;
|
||||
readonly has_user_api_key: boolean;
|
||||
readonly has_central_api_key_fallback: boolean;
|
||||
readonly byok_enabled: boolean;
|
||||
|
||||
@@ -20,7 +20,7 @@ import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
import { RequirePermission } from "#/modules/permissions/RequirePermission";
|
||||
import { providerTypeByIDFromConfigs } from "#/pages/AgentsPage/utils/modelOptions";
|
||||
import { providerInfoByIDFromConfigs } from "#/pages/AgentsPage/utils/modelOptions";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { CoderAgentsPageView } from "./CoderAgentsPageView";
|
||||
|
||||
@@ -114,7 +114,7 @@ const CoderAgentsPage: FC = () => {
|
||||
updateChatComputerUseProvider(queryClient),
|
||||
);
|
||||
|
||||
const providerTypeByID = providerTypeByIDFromConfigs(
|
||||
const providerInfoByID = providerInfoByIDFromConfigs(
|
||||
providerConfigsQuery.data,
|
||||
);
|
||||
|
||||
@@ -143,7 +143,7 @@ const CoderAgentsPage: FC = () => {
|
||||
titleGenerationModelOverrideData={titleGenerationModelQuery.data}
|
||||
exploreModelOverrideData={exploreModelOverrideQuery.data}
|
||||
modelConfigsData={modelConfigsQuery.data}
|
||||
providerTypeByID={providerTypeByID}
|
||||
providerInfoByID={providerInfoByID}
|
||||
modelConfigsError={modelConfigsQuery.error}
|
||||
isLoadingModelConfigs={modelConfigsQuery.isLoading}
|
||||
isFetchingModelConfigs={modelConfigsQuery.isFetching}
|
||||
|
||||
@@ -104,9 +104,12 @@ const allModelConfigs: TypesGen.ChatModelConfig[] = [
|
||||
exploreDisabledModelConfig,
|
||||
];
|
||||
|
||||
const providerTypeByID = new Map<string, string>([
|
||||
["provider-1", "openai"],
|
||||
["provider-anthropic", "anthropic"],
|
||||
const providerInfoByID = new Map([
|
||||
["provider-1", { provider: "openai", displayName: "OpenAI", icon: "" }],
|
||||
[
|
||||
"provider-anthropic",
|
||||
{ provider: "anthropic", displayName: "Anthropic", icon: "" },
|
||||
],
|
||||
]);
|
||||
|
||||
const buildArgs = (
|
||||
@@ -123,7 +126,7 @@ const buildArgs = (
|
||||
titleGenerationModelOverrideData: buildTitleGenerationModelOverrideData(),
|
||||
exploreModelOverrideData: buildOverrideData("explore"),
|
||||
modelConfigsData: allModelConfigs,
|
||||
providerTypeByID,
|
||||
providerInfoByID,
|
||||
modelConfigsError: undefined,
|
||||
isLoadingModelConfigs: false,
|
||||
isFetchingModelConfigs: false,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { AdvisorSettings } from "#/pages/AgentsPage/components/AdvisorSettings";
|
||||
import { VirtualDesktopSettings } from "#/pages/AgentsPage/components/VirtualDesktopSettings";
|
||||
import type { ProviderInfo } from "#/pages/AgentsPage/utils/modelOptions";
|
||||
import {
|
||||
AdminPersonalModelOverridesSettings,
|
||||
type SavePersonalModelOverridesAdminSetting,
|
||||
@@ -34,7 +35,7 @@ export interface CoderAgentsPageViewProps {
|
||||
titleGenerationModelOverrideData?: TypesGen.ChatModelOverrideResponse;
|
||||
exploreModelOverrideData?: TypesGen.ChatModelOverrideResponse;
|
||||
modelConfigsData: TypesGen.ChatModelConfig[] | undefined;
|
||||
providerTypeByID: ReadonlyMap<string, string>;
|
||||
providerInfoByID: ReadonlyMap<string, ProviderInfo>;
|
||||
modelConfigsError: unknown;
|
||||
isLoadingModelConfigs: boolean;
|
||||
isFetchingModelConfigs: boolean;
|
||||
@@ -84,7 +85,7 @@ export const CoderAgentsPageView: FC<CoderAgentsPageViewProps> = ({
|
||||
titleGenerationModelOverrideData,
|
||||
exploreModelOverrideData,
|
||||
modelConfigsData,
|
||||
providerTypeByID,
|
||||
providerInfoByID,
|
||||
modelConfigsError,
|
||||
isLoadingModelConfigs,
|
||||
isFetchingModelConfigs,
|
||||
@@ -147,7 +148,7 @@ export const CoderAgentsPageView: FC<CoderAgentsPageViewProps> = ({
|
||||
description="Used by delegated agents that can edit files or run commands."
|
||||
modelOverrideData={generalModelOverrideData}
|
||||
enabledModelConfigs={enabledModelConfigs}
|
||||
providerTypeByID={providerTypeByID}
|
||||
providerInfoByID={providerInfoByID}
|
||||
modelConfigsError={modelConfigsError}
|
||||
isLoading={isLoadingModelConfigs}
|
||||
onSaveModelOverride={onSaveGeneralModelOverride}
|
||||
@@ -161,7 +162,7 @@ export const CoderAgentsPageView: FC<CoderAgentsPageViewProps> = ({
|
||||
description="Leave unset to use Coder's title default, which prefers fast models from configured providers."
|
||||
modelOverrideData={titleGenerationModelOverrideData}
|
||||
enabledModelConfigs={enabledModelConfigs}
|
||||
providerTypeByID={providerTypeByID}
|
||||
providerInfoByID={providerInfoByID}
|
||||
modelConfigsError={modelConfigsError}
|
||||
isLoading={isLoadingModelConfigs}
|
||||
onSaveModelOverride={onSaveTitleGenerationModel}
|
||||
@@ -176,7 +177,7 @@ export const CoderAgentsPageView: FC<CoderAgentsPageViewProps> = ({
|
||||
description="Used for read-only codebase exploration before work returns to the main agent."
|
||||
modelOverrideData={exploreModelOverrideData}
|
||||
enabledModelConfigs={enabledModelConfigs}
|
||||
providerTypeByID={providerTypeByID}
|
||||
providerInfoByID={providerInfoByID}
|
||||
modelConfigsError={modelConfigsError}
|
||||
isLoading={isLoadingModelConfigs}
|
||||
onSaveModelOverride={onSaveExploreModelOverride}
|
||||
|
||||
+16
-17
@@ -3,9 +3,9 @@ import type { FC, ReactNode } from "react";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { useTemporarySavedState } from "#/components/TemporarySavedState/TemporarySavedState";
|
||||
import type { ModelSelectorOption } from "#/pages/AgentsPage/components/ChatElements/ModelSelector";
|
||||
import { ModelSelector } from "#/pages/AgentsPage/components/ChatElements/ModelSelector";
|
||||
import { ModelOverrideAlerts } from "#/pages/AgentsPage/components/ModelOverrideAlerts";
|
||||
import type { ProviderInfo } from "#/pages/AgentsPage/utils/modelOptions";
|
||||
import { AgentSettingLayout } from "./AgentSettingLayout";
|
||||
|
||||
export interface MutationCallbacks {
|
||||
@@ -27,7 +27,7 @@ interface SubagentModelOverrideSettingsProps {
|
||||
description?: ReactNode;
|
||||
modelOverrideData: ModelOverrideData | undefined;
|
||||
enabledModelConfigs: readonly TypesGen.ChatModelConfig[];
|
||||
providerTypeByID: ReadonlyMap<string, string>;
|
||||
providerInfoByID: ReadonlyMap<string, ProviderInfo>;
|
||||
modelConfigsError: unknown;
|
||||
isLoading: boolean;
|
||||
onSaveModelOverride: (
|
||||
@@ -42,17 +42,6 @@ interface SubagentModelOverrideSettingsProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const toModelSelectorOption = (
|
||||
modelConfig: TypesGen.ChatModelConfig,
|
||||
providerTypeByID: ReadonlyMap<string, string>,
|
||||
): ModelSelectorOption => ({
|
||||
id: modelConfig.id,
|
||||
provider: providerTypeByID.get(modelConfig.ai_provider_id) ?? "",
|
||||
model: modelConfig.model,
|
||||
displayName: modelConfig.display_name.trim() || modelConfig.model,
|
||||
contextLimit: modelConfig.context_limit,
|
||||
});
|
||||
|
||||
export const SubagentModelOverrideSettings: FC<
|
||||
SubagentModelOverrideSettingsProps
|
||||
> = ({
|
||||
@@ -60,7 +49,7 @@ export const SubagentModelOverrideSettings: FC<
|
||||
description,
|
||||
modelOverrideData,
|
||||
enabledModelConfigs,
|
||||
providerTypeByID,
|
||||
providerInfoByID,
|
||||
modelConfigsError,
|
||||
isLoading,
|
||||
onSaveModelOverride,
|
||||
@@ -74,9 +63,19 @@ export const SubagentModelOverrideSettings: FC<
|
||||
const { isSavedVisible, showSavedState } = useTemporarySavedState();
|
||||
const hasLoadedModelOverride = modelOverrideData !== undefined;
|
||||
const isMalformedOverride = modelOverrideData?.is_malformed ?? false;
|
||||
const enabledModelOptions = enabledModelConfigs.map((modelConfig) =>
|
||||
toModelSelectorOption(modelConfig, providerTypeByID),
|
||||
);
|
||||
const enabledModelOptions = enabledModelConfigs.map((modelConfig) => {
|
||||
const providerInfo = providerInfoByID.get(modelConfig.ai_provider_id);
|
||||
return {
|
||||
id: modelConfig.id,
|
||||
provider: providerInfo?.provider ?? "",
|
||||
providerId: modelConfig.ai_provider_id,
|
||||
providerLabel: providerInfo?.displayName,
|
||||
providerIcon: providerInfo?.icon,
|
||||
model: modelConfig.model,
|
||||
displayName: modelConfig.display_name.trim() || modelConfig.model,
|
||||
contextLimit: modelConfig.context_limit,
|
||||
};
|
||||
});
|
||||
|
||||
const form = useFormik({
|
||||
enableReinitialize: true,
|
||||
|
||||
@@ -22,7 +22,6 @@ interface IconPickerFieldProps {
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onPickEmoji: (value: string) => void;
|
||||
}
|
||||
|
||||
export const IconPickerField: FC<IconPickerFieldProps> = ({
|
||||
@@ -31,7 +30,6 @@ export const IconPickerField: FC<IconPickerFieldProps> = ({
|
||||
placeholder,
|
||||
disabled,
|
||||
onChange,
|
||||
onPickEmoji,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const hasIcon = value !== "";
|
||||
@@ -76,12 +74,20 @@ export const IconPickerField: FC<IconPickerFieldProps> = ({
|
||||
<AnimatedChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="bottom" align="end" className="w-min">
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
className="w-min"
|
||||
// The popover is portaled in the DOM but still a React child of
|
||||
// InputGroupAddon, whose click handler focuses the text input.
|
||||
// Stop clicks here so the emoji picker keeps focus.
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<EmojiPicker
|
||||
onEmojiSelect={(emoji) => {
|
||||
const picked = emoji.src ?? `/emojis/${emoji.unified}.png`;
|
||||
onPickEmoji(picked);
|
||||
onChange(picked);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -155,9 +155,6 @@ export const MCPServerFormFields: FC<MCPServerFormFieldsProps> = ({
|
||||
value={form.values.iconURL}
|
||||
placeholder="file location"
|
||||
onChange={(value) => void form.setFieldValue("iconURL", value)}
|
||||
onPickEmoji={(value) =>
|
||||
void form.setFieldValue("iconURL", value)
|
||||
}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -7,6 +7,7 @@ const MockOpenAIProviderConfig: ChatProviderConfig = {
|
||||
id: "prov-openai",
|
||||
provider: "openai",
|
||||
display_name: "OpenAI",
|
||||
icon: "",
|
||||
enabled: true,
|
||||
has_api_key: true,
|
||||
central_api_key_enabled: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
@@ -25,6 +26,8 @@ const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const createMutation = useMutation(createAIProviderMutation(queryClient));
|
||||
const defaultIcon = getProviderIcon(provider.value) ?? "";
|
||||
const [icon, setIcon] = useState(defaultIcon);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -36,11 +39,7 @@ const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
|
||||
</Link>
|
||||
<div className="flex flex-col gap-6 pt-6">
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<Avatar
|
||||
variant="icon"
|
||||
size="lg"
|
||||
src={getProviderIcon(provider.value)}
|
||||
/>
|
||||
<Avatar variant="icon" size="lg" src={icon || defaultIcon} />
|
||||
<SettingsHeaderTitle>{`Add ${indefiniteArticle(
|
||||
provider.label,
|
||||
)} ${provider.label} provider`}</SettingsHeaderTitle>
|
||||
@@ -51,7 +50,8 @@ const AddProviderPageView: React.FC<AddProviderPageViewProps> = ({
|
||||
<div className="border border-solid p-6 rounded-lg">
|
||||
<ProviderForm
|
||||
editing={false}
|
||||
initialValues={{ type: provider.value }}
|
||||
initialValues={{ type: provider.value, icon: defaultIcon }}
|
||||
onIconChange={setIcon}
|
||||
isLoading={createMutation.isPending}
|
||||
submitError={createMutation.error}
|
||||
onSubmit={async (values) => {
|
||||
|
||||
+3
-1
@@ -146,7 +146,9 @@ const UpdateProviderPageView: React.FC = () => {
|
||||
<Avatar
|
||||
variant="icon"
|
||||
size="lg"
|
||||
src={getProviderIcon(getProviderDisplayType(provider))}
|
||||
src={
|
||||
provider.icon || getProviderIcon(getProviderDisplayType(provider))
|
||||
}
|
||||
/>
|
||||
<SettingsHeaderTitle>
|
||||
<span className="block min-w-0 truncate">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Label } from "#/components/Label/Label";
|
||||
import { Link as DocsLink } from "#/components/Link/Link";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { useUnsavedChangesPrompt } from "#/hooks/useUnsavedChangesPrompt";
|
||||
import { IconPickerField } from "#/pages/AISettingsPage/MCPServersPage/components/IconPickerField";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { getFormHelpers } from "#/utils/formUtils";
|
||||
import { CredentialField } from "./CredentialField";
|
||||
@@ -22,6 +23,7 @@ export type ProviderFormValues = {
|
||||
type: AIProviderType | "";
|
||||
name: string;
|
||||
displayName: string;
|
||||
icon: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
smallFastModel: string;
|
||||
@@ -64,6 +66,7 @@ const defaultInitialValues: ProviderFormValues = {
|
||||
type: "anthropic",
|
||||
name: "",
|
||||
displayName: "",
|
||||
icon: "",
|
||||
baseUrl: "",
|
||||
model: "",
|
||||
smallFastModel: "",
|
||||
@@ -132,6 +135,7 @@ const makeOpenAiAnthropicSchema = (editing: boolean) =>
|
||||
.required(),
|
||||
name: makeNameSchema(editing),
|
||||
displayName: makeDisplayNameSchema(editing),
|
||||
icon: Yup.string(),
|
||||
baseUrl: Yup.string()
|
||||
.url("Endpoint must be a valid URL")
|
||||
.matches(HTTP_SCHEME_REGEX, "Endpoint must use http or https.")
|
||||
@@ -162,6 +166,7 @@ const makeBedrockSchema = (editing: boolean) =>
|
||||
.required(),
|
||||
name: makeNameSchema(editing),
|
||||
displayName: makeDisplayNameSchema(editing),
|
||||
icon: Yup.string(),
|
||||
baseUrl: Yup.string()
|
||||
.url("Endpoint must be a valid URL")
|
||||
.matches(
|
||||
@@ -199,6 +204,7 @@ const makeCopilotSchema = (editing: boolean) =>
|
||||
.required(),
|
||||
name: makeNameSchema(editing),
|
||||
displayName: makeDisplayNameSchema(editing),
|
||||
icon: Yup.string(),
|
||||
baseUrl: Yup.string()
|
||||
.url("Endpoint must be a valid URL")
|
||||
.matches(HTTP_SCHEME_REGEX, "Endpoint must use http or https.")
|
||||
@@ -251,6 +257,8 @@ type ProviderFormProps = {
|
||||
/** Masked rendering of the saved openai/anthropic key (e.g. `sk-***...ABCD`). Falls back to a generic mask when omitted. */
|
||||
openAiAnthropicMaskedApiKey?: string;
|
||||
initialValues?: Partial<ProviderFormValues>;
|
||||
/** Fires whenever the icon field changes, so page headers can preview it. */
|
||||
onIconChange?: (icon: string) => void;
|
||||
onSubmit?: (values: ProviderFormValues) => void;
|
||||
isLoading?: boolean;
|
||||
submitError?: unknown;
|
||||
@@ -279,6 +287,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
openAiAnthropicSavedApiKey = false,
|
||||
openAiAnthropicMaskedApiKey,
|
||||
initialValues,
|
||||
onIconChange,
|
||||
onSubmit,
|
||||
isLoading = false,
|
||||
submitError,
|
||||
@@ -322,6 +331,25 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
});
|
||||
const getFieldHelpers = getFormHelpers(form, submitError);
|
||||
|
||||
const handleIconChange = (value: string) => {
|
||||
void form.setFieldValue("icon", value);
|
||||
onIconChange?.(value);
|
||||
};
|
||||
|
||||
const iconField = (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="icon">Icon</Label>
|
||||
<div className="text-xs text-content-secondary">
|
||||
Optional. URL or emoji shown for this provider.
|
||||
</div>
|
||||
<IconPickerField
|
||||
id="icon"
|
||||
value={form.values.icon}
|
||||
onChange={handleIconChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const typeSelectValue = form.values.type;
|
||||
|
||||
// Clears the field once if it's still showing the seeded mask;
|
||||
@@ -403,6 +431,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
{iconField}
|
||||
<FormField
|
||||
required
|
||||
field={getFieldHelpers("baseUrl")}
|
||||
@@ -462,6 +491,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
{iconField}
|
||||
<FormField
|
||||
required
|
||||
field={getFieldHelpers("baseUrl")}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
|
||||
|
||||
type ProviderIconProps = {
|
||||
provider: string;
|
||||
icon?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const getProviderIcon = (provider: string): string | undefined => {
|
||||
@@ -51,13 +53,20 @@ const getProviderName = (provider: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
export const ProviderIcon: React.FC<ProviderIconProps> = ({ provider }) => {
|
||||
const iconSrc = getProviderIcon(provider);
|
||||
export const ProviderIcon: React.FC<ProviderIconProps> = ({
|
||||
provider,
|
||||
icon,
|
||||
className = "size-icon-sm",
|
||||
}) => {
|
||||
const iconSrc = icon || getProviderIcon(provider);
|
||||
const name = getProviderName(provider);
|
||||
if (iconSrc === undefined) {
|
||||
return (
|
||||
<Building2Icon className="size-icon-sm flex-shrink-0" aria-label={name} />
|
||||
<Building2Icon
|
||||
className={`${className} flex-shrink-0`}
|
||||
aria-label={name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <ExternalImage src={iconSrc} alt={name} className="size-icon-sm" />;
|
||||
return <ExternalImage src={iconSrc} alt={name} className={className} />;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,10 @@ export const ProviderRow: React.FC<ProviderRowProps> = ({
|
||||
size="lg"
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
>
|
||||
<ProviderIcon provider={getProviderDisplayType(provider)} />
|
||||
<ProviderIcon
|
||||
provider={getProviderDisplayType(provider)}
|
||||
icon={provider.icon}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -25,6 +25,7 @@ const baseOpenAIFormValues: ProviderFormValues = {
|
||||
type: "openai",
|
||||
name: "primary-openai",
|
||||
displayName: "Primary OpenAI",
|
||||
icon: "",
|
||||
baseUrl: "https://api.openai.com",
|
||||
model: "",
|
||||
smallFastModel: "",
|
||||
@@ -39,6 +40,7 @@ const baseBedrockFormValues: ProviderFormValues = {
|
||||
type: "bedrock",
|
||||
name: "primary-bedrock",
|
||||
displayName: "Primary Bedrock",
|
||||
icon: "",
|
||||
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
||||
model: "anthropic.claude-sonnet-4-5",
|
||||
smallFastModel: "anthropic.claude-haiku-4-5",
|
||||
@@ -53,6 +55,7 @@ const baseCopilotFormValues: ProviderFormValues = {
|
||||
type: "copilot",
|
||||
name: "copilot",
|
||||
displayName: "GitHub Copilot",
|
||||
icon: "",
|
||||
baseUrl: "https://api.business.githubcopilot.com",
|
||||
model: "",
|
||||
smallFastModel: "",
|
||||
@@ -319,6 +322,22 @@ describe("providerFormValuesToCreate", () => {
|
||||
expect(req.display_name).toBeUndefined();
|
||||
});
|
||||
|
||||
it("trims and sends icon when provided", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseOpenAIFormValues,
|
||||
icon: " https://example.com/openai.svg ",
|
||||
});
|
||||
expect(req.icon).toBe("https://example.com/openai.svg");
|
||||
});
|
||||
|
||||
it("omits icon when blank", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseOpenAIFormValues,
|
||||
icon: " ",
|
||||
});
|
||||
expect(req.icon).toBeUndefined();
|
||||
});
|
||||
|
||||
it("trims whitespace from name and baseUrl", () => {
|
||||
const req = providerFormValuesToCreate({
|
||||
...baseOpenAIFormValues,
|
||||
@@ -543,6 +562,14 @@ describe("providerFormValuesToUpdate", () => {
|
||||
);
|
||||
expect(req.api_keys).toEqual([]);
|
||||
});
|
||||
|
||||
it("sends a trimmed icon so blank clears the stored icon", () => {
|
||||
const req = providerFormValuesToUpdate(
|
||||
{ ...baseOpenAIFormValues, icon: " " },
|
||||
MockAIProviderOpenAI,
|
||||
);
|
||||
expect(req.icon).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bedrock", () => {
|
||||
@@ -664,10 +691,14 @@ describe("providerFormValuesToUpdate", () => {
|
||||
|
||||
describe("aiProviderToFormValues", () => {
|
||||
it("seeds OpenAI form values from a wire provider", () => {
|
||||
const values = aiProviderToFormValues(MockAIProviderOpenAI);
|
||||
const values = aiProviderToFormValues({
|
||||
...MockAIProviderOpenAI,
|
||||
icon: "https://example.com/openai.svg",
|
||||
});
|
||||
expect(values.type).toBe("openai");
|
||||
expect(values.name).toBe(MockAIProviderOpenAI.name);
|
||||
expect(values.baseUrl).toBe(MockAIProviderOpenAI.base_url);
|
||||
expect(values.icon).toBe("https://example.com/openai.svg");
|
||||
expect(values.apiKey).toBe("");
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
parseBedrockRegionFromBaseUrl,
|
||||
SAVED_CREDENTIAL_MASK,
|
||||
} from "./ProviderForm";
|
||||
import { getProviderIcon } from "./ProviderIcon";
|
||||
|
||||
/** Drop placeholder masks so they don't round-trip back to the API. */
|
||||
const sanitizeCredential = (
|
||||
@@ -137,9 +138,11 @@ export const providerFormValuesToCreate = (
|
||||
values: ProviderFormValues,
|
||||
): CreateAIProviderRequest => {
|
||||
const displayName = values.displayName.trim();
|
||||
const icon = values.icon.trim();
|
||||
const base: Omit<CreateAIProviderRequest, "type"> = {
|
||||
name: values.name.trim(),
|
||||
...(displayName ? { display_name: displayName } : {}),
|
||||
...(icon ? { icon } : {}),
|
||||
base_url: values.baseUrl.trim(),
|
||||
enabled: values.enabled,
|
||||
};
|
||||
@@ -188,6 +191,7 @@ export const providerFormValuesToUpdate = (
|
||||
): UpdateAIProviderRequest => {
|
||||
const base: UpdateAIProviderRequest = {
|
||||
display_name: values.displayName.trim(),
|
||||
icon: values.icon.trim(),
|
||||
enabled: values.enabled,
|
||||
base_url: values.baseUrl.trim(),
|
||||
};
|
||||
@@ -246,6 +250,7 @@ export const aiProviderToFormValues = (
|
||||
type: "bedrock",
|
||||
name: provider.name,
|
||||
displayName,
|
||||
icon: provider.icon || (getProviderIcon("bedrock") ?? ""),
|
||||
baseUrl: provider.base_url,
|
||||
model: s.model ?? "",
|
||||
smallFastModel: s.small_fast_model ?? "",
|
||||
@@ -261,15 +266,18 @@ export const aiProviderToFormValues = (
|
||||
type: "copilot",
|
||||
name: provider.name,
|
||||
displayName,
|
||||
icon: provider.icon || (getProviderIcon("copilot") ?? ""),
|
||||
baseUrl: provider.base_url,
|
||||
enabled: provider.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
const displayType = getProviderDisplayType(provider);
|
||||
return {
|
||||
type: getProviderDisplayType(provider),
|
||||
type: displayType,
|
||||
name: provider.name,
|
||||
displayName,
|
||||
icon: provider.icon || (getProviderIcon(displayType) ?? ""),
|
||||
baseUrl: provider.base_url,
|
||||
apiKey: "",
|
||||
enabled: provider.enabled,
|
||||
|
||||
@@ -821,6 +821,7 @@ const meta: Meta<typeof AgentChatPageLayout> = {
|
||||
type: "openai",
|
||||
name: "openai",
|
||||
display_name: "OpenAI",
|
||||
icon: "",
|
||||
enabled: true,
|
||||
deleted: false,
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ const createProvider = (
|
||||
provider_id: overrides.provider_id,
|
||||
provider: overrides.provider,
|
||||
display_name: overrides.display_name ?? overrides.provider,
|
||||
icon: overrides.icon ?? "",
|
||||
has_user_api_key: overrides.has_user_api_key ?? false,
|
||||
has_central_api_key_fallback: overrides.has_central_api_key_fallback ?? false,
|
||||
byok_enabled: overrides.byok_enabled ?? true,
|
||||
|
||||
@@ -70,7 +70,7 @@ import {
|
||||
import { maybePlayChime } from "./utils/chime";
|
||||
import {
|
||||
getModelOptionsFromConfigs,
|
||||
providerTypeByIDFromUserConfigs,
|
||||
providerInfoByIDFromUserConfigs,
|
||||
} from "./utils/modelOptions";
|
||||
import { clearPersistedRightPanelState } from "./utils/rightPanelTabStorage";
|
||||
import { clearPersistedSidebarTabId } from "./utils/sidebarTabStorage";
|
||||
@@ -331,7 +331,7 @@ const AgentsPage: FC = () => {
|
||||
const catalogModelOptions = getModelOptionsFromConfigs(
|
||||
chatModelConfigsQuery.data,
|
||||
chatModelsQuery.data,
|
||||
providerTypeByIDFromUserConfigs(chatProviderConfigsQuery.data),
|
||||
providerInfoByIDFromUserConfigs(chatProviderConfigsQuery.data),
|
||||
);
|
||||
const chatList = chatsQuery.data?.pages.flat() ?? [];
|
||||
const isArchiving =
|
||||
|
||||
@@ -180,7 +180,7 @@ const AgentsRouteElement = () => (
|
||||
is_malformed: false,
|
||||
}}
|
||||
modelConfigsData={[]}
|
||||
providerTypeByID={new Map()}
|
||||
providerInfoByID={new Map()}
|
||||
modelConfigsError={undefined}
|
||||
isLoadingModelConfigs={false}
|
||||
isFetchingModelConfigs={false}
|
||||
@@ -453,6 +453,7 @@ const meta: Meta<typeof AgentsPageView> = {
|
||||
type: "openai",
|
||||
name: "openai",
|
||||
display_name: "OpenAI",
|
||||
icon: "",
|
||||
enabled: true,
|
||||
deleted: false,
|
||||
},
|
||||
|
||||
@@ -128,6 +128,36 @@ export const MultipleProvidersWithCustomLabel: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleProviderInstances: Story = {
|
||||
args: {
|
||||
options: [
|
||||
...openAIModels,
|
||||
{
|
||||
...MockModelSelectorOption,
|
||||
id: "anthropic-primary/claude-sonnet-4",
|
||||
provider: "anthropic",
|
||||
providerId: "provider-anthropic-primary",
|
||||
providerLabel: "Anthropic",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
displayName: "Claude Sonnet 4",
|
||||
contextLimit: 200_000,
|
||||
},
|
||||
{
|
||||
...MockModelSelectorOption,
|
||||
id: "anthropic-hyper/claude-opus-4",
|
||||
provider: "anthropic",
|
||||
providerId: "provider-anthropic-hyper",
|
||||
providerLabel: "Hyper",
|
||||
providerIcon: "/icon/coder.svg",
|
||||
model: "claude-opus-4-20250514",
|
||||
displayName: "Claude Opus 4",
|
||||
contextLimit: 200_000,
|
||||
},
|
||||
],
|
||||
value: "anthropic-primary/claude-sonnet-4",
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Empty state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { render } from "#/testHelpers/renderHelpers";
|
||||
import { ModelSelector, type ModelSelectorOption } from "./ModelSelector";
|
||||
import { MockModelSelectorOption } from "./modelSelectorFixtures";
|
||||
|
||||
|
||||
@@ -15,12 +15,16 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "#/components/Popover/Popover";
|
||||
import { ProviderIcon } from "#/pages/AISettingsPage/ProvidersPage/components/ProviderIcon";
|
||||
import { formatProviderLabel as defaultFormatProviderLabel } from "#/utils/aiProviders";
|
||||
import { cn } from "#/utils/cn";
|
||||
|
||||
export interface ModelSelectorOption {
|
||||
id: string;
|
||||
provider: string;
|
||||
providerId?: string;
|
||||
providerLabel?: string;
|
||||
providerIcon?: string;
|
||||
model: string;
|
||||
displayName: string;
|
||||
contextLimit?: number;
|
||||
@@ -51,6 +55,11 @@ const formatContextLimit = (tokens: number): string => {
|
||||
return `${k}K`;
|
||||
};
|
||||
|
||||
const getProviderLabel = (
|
||||
option: ModelSelectorOption,
|
||||
formatProviderLabel: (provider: string) => string,
|
||||
) => option.providerLabel?.trim() || formatProviderLabel(option.provider);
|
||||
|
||||
const getSearchText = (option: ModelSelectorOption, providerLabel: string) =>
|
||||
[
|
||||
providerLabel,
|
||||
@@ -92,17 +101,18 @@ export const ModelSelector: FC<ModelSelectorProps> = ({
|
||||
const grouped = new Map<string, ModelSelectorOption[]>();
|
||||
|
||||
for (const option of options) {
|
||||
const providerLabel = formatProviderLabel(option.provider);
|
||||
const providerLabel = getProviderLabel(option, formatProviderLabel);
|
||||
if (query && !getSearchText(option, providerLabel).includes(query)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const providerOptions = grouped.get(option.provider);
|
||||
const groupKey = option.providerId?.trim() || option.provider;
|
||||
const providerOptions = grouped.get(groupKey);
|
||||
if (providerOptions) {
|
||||
providerOptions.push(option);
|
||||
continue;
|
||||
}
|
||||
grouped.set(option.provider, [option]);
|
||||
grouped.set(groupKey, [option]);
|
||||
}
|
||||
|
||||
return Array.from(grouped.entries());
|
||||
@@ -173,29 +183,45 @@ export const ModelSelector: FC<ModelSelectorProps> = ({
|
||||
<CommandEmpty className="py-3 text-xs font-normal leading-[18px] text-content-secondary">
|
||||
{emptyMessage}
|
||||
</CommandEmpty>
|
||||
{optionsByProvider.map(([provider, providerOptions], index) => (
|
||||
<CommandGroup
|
||||
key={provider}
|
||||
heading={formatProviderLabel(provider)}
|
||||
className={cn(
|
||||
"p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:leading-[18px] [&_[cmdk-group-heading]]:text-content-secondary",
|
||||
index > 0 &&
|
||||
"border-0 border-t border-solid border-border-default",
|
||||
)}
|
||||
>
|
||||
{providerOptions.map((option) => (
|
||||
<ModelOptionItem
|
||||
key={option.id}
|
||||
option={option}
|
||||
isSelected={option.id === value}
|
||||
onSelect={() => {
|
||||
onValueChange(option.id);
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
{optionsByProvider.map(([providerKey, providerOptions], index) => {
|
||||
const firstOption = providerOptions[0];
|
||||
const providerLabel = getProviderLabel(
|
||||
firstOption,
|
||||
formatProviderLabel,
|
||||
);
|
||||
return (
|
||||
<CommandGroup
|
||||
key={providerKey}
|
||||
heading={
|
||||
<span className="flex items-center gap-1.5">
|
||||
<ProviderIcon
|
||||
provider={firstOption.provider}
|
||||
icon={firstOption.providerIcon}
|
||||
className="size-3.5"
|
||||
/>
|
||||
<span>{providerLabel}</span>
|
||||
</span>
|
||||
}
|
||||
className={cn(
|
||||
"p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:leading-[18px] [&_[cmdk-group-heading]]:text-content-secondary",
|
||||
index > 0 &&
|
||||
"border-0 border-t border-solid border-border-default",
|
||||
)}
|
||||
>
|
||||
{providerOptions.map((option) => (
|
||||
<ModelOptionItem
|
||||
key={option.id}
|
||||
option={option}
|
||||
isSelected={option.id === value}
|
||||
onSelect={() => {
|
||||
onValueChange(option.id);
|
||||
handleOpenChange(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup>
|
||||
);
|
||||
})}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
|
||||
@@ -2097,8 +2097,10 @@ export const SettingsAPIKeysNonAdmin: Story = {
|
||||
provider_id: "prov-1",
|
||||
provider: "openai",
|
||||
display_name: "OpenAI",
|
||||
icon: "",
|
||||
has_user_api_key: false,
|
||||
has_central_api_key_fallback: false,
|
||||
byok_enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
getUnsupportedProviderNames,
|
||||
hasConfiguredProviderConfigs,
|
||||
hasUserFixableProviders,
|
||||
providerInfoByIDFromConfigs,
|
||||
providerInfoByIDFromUserConfigs,
|
||||
providerTypeByIDFromConfigs,
|
||||
providerTypeByIDFromUserConfigs,
|
||||
resolveModelOptionId,
|
||||
@@ -34,10 +36,16 @@ const createConfig = (
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const providerTypeByID = new Map<string, string>([
|
||||
["prov-openai", "openai"],
|
||||
["prov-anthropic", "anthropic"],
|
||||
["prov-openrouter", "openrouter"],
|
||||
const providerInfoByID = new Map([
|
||||
["prov-openai", { provider: "openai", displayName: "OpenAI", icon: "" }],
|
||||
[
|
||||
"prov-anthropic",
|
||||
{ provider: "anthropic", displayName: "Anthropic", icon: "" },
|
||||
],
|
||||
[
|
||||
"prov-openrouter",
|
||||
{ provider: "openrouter", displayName: "OpenRouter", icon: "" },
|
||||
],
|
||||
]);
|
||||
|
||||
const createCatalog = (
|
||||
@@ -240,12 +248,18 @@ describe("resolveModelOptionId", () => {
|
||||
{
|
||||
id: "config-1",
|
||||
provider: "openai",
|
||||
providerId: "prov-openai",
|
||||
providerLabel: "OpenAI",
|
||||
providerIcon: "",
|
||||
model: "gpt-4o",
|
||||
displayName: "GPT-4o",
|
||||
},
|
||||
{
|
||||
id: "config-2",
|
||||
provider: "anthropic",
|
||||
providerId: "prov-anthropic",
|
||||
providerLabel: "Anthropic",
|
||||
providerIcon: "",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
displayName: "Claude Sonnet",
|
||||
},
|
||||
@@ -293,22 +307,19 @@ describe("getModelOptionsFromConfigs", () => {
|
||||
]);
|
||||
|
||||
expect(
|
||||
getModelOptionsFromConfigs(configs, catalog, providerTypeByID),
|
||||
getModelOptionsFromConfigs(configs, catalog, providerInfoByID),
|
||||
).toEqual([
|
||||
{
|
||||
id: "config-1",
|
||||
provider: "openai",
|
||||
providerId: "prov-openai",
|
||||
providerLabel: "OpenAI",
|
||||
providerIcon: "",
|
||||
model: "gpt-4o",
|
||||
displayName: "GPT-4o (Fast)",
|
||||
contextLimit: 128_000,
|
||||
},
|
||||
{
|
||||
id: "config-2",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
displayName: "GPT-4o (Quality)",
|
||||
contextLimit: 128_000,
|
||||
},
|
||||
expect.objectContaining({ id: "config-2" }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -331,7 +342,7 @@ describe("getModelOptionsFromConfigs", () => {
|
||||
]);
|
||||
|
||||
expect(
|
||||
getModelOptionsFromConfigs(configs, catalog, providerTypeByID),
|
||||
getModelOptionsFromConfigs(configs, catalog, providerInfoByID),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -362,16 +373,10 @@ describe("getModelOptionsFromConfigs", () => {
|
||||
]);
|
||||
|
||||
expect(
|
||||
getModelOptionsFromConfigs(configs, catalog, providerTypeByID),
|
||||
).toEqual([
|
||||
{
|
||||
id: "config-2",
|
||||
provider: "openai",
|
||||
model: "gpt-4.1",
|
||||
displayName: "GPT-4.1",
|
||||
contextLimit: 128_000,
|
||||
},
|
||||
]);
|
||||
getModelOptionsFromConfigs(configs, catalog, providerInfoByID).map(
|
||||
(option) => option.id,
|
||||
),
|
||||
).toEqual(["config-2"]);
|
||||
});
|
||||
|
||||
it("falls back to the model name when display_name is blank", () => {
|
||||
@@ -393,24 +398,22 @@ describe("getModelOptionsFromConfigs", () => {
|
||||
]);
|
||||
|
||||
expect(
|
||||
getModelOptionsFromConfigs(configs, catalog, providerTypeByID),
|
||||
getModelOptionsFromConfigs(configs, catalog, providerInfoByID),
|
||||
).toEqual([
|
||||
{
|
||||
expect.objectContaining({
|
||||
id: "config-1",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
displayName: "gpt-4o",
|
||||
contextLimit: 0,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an empty array for null and undefined inputs", () => {
|
||||
expect(getModelOptionsFromConfigs(null, null, providerTypeByID)).toEqual(
|
||||
expect(getModelOptionsFromConfigs(null, null, providerInfoByID)).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(
|
||||
getModelOptionsFromConfigs(undefined, undefined, providerTypeByID),
|
||||
getModelOptionsFromConfigs(undefined, undefined, providerInfoByID),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -452,7 +455,7 @@ describe("getModelOptionsFromConfigs", () => {
|
||||
]);
|
||||
|
||||
expect(
|
||||
getModelOptionsFromConfigs(configs, catalog, providerTypeByID).map(
|
||||
getModelOptionsFromConfigs(configs, catalog, providerInfoByID).map(
|
||||
(option) => option.id,
|
||||
),
|
||||
).toEqual([
|
||||
@@ -488,23 +491,10 @@ describe("getModelOptionsFromConfigs", () => {
|
||||
]);
|
||||
|
||||
expect(
|
||||
getModelOptionsFromConfigs(configs, catalog, providerTypeByID),
|
||||
).toEqual([
|
||||
{
|
||||
id: "config-2",
|
||||
provider: "openrouter",
|
||||
model: "anthropic/claude-sonnet-4-20250514",
|
||||
displayName: "Claude via OpenRouter",
|
||||
contextLimit: 200_000,
|
||||
},
|
||||
{
|
||||
id: "config-1",
|
||||
provider: "openrouter",
|
||||
model: "openai/gpt-4o",
|
||||
displayName: "GPT-4o via OpenRouter",
|
||||
contextLimit: 128_000,
|
||||
},
|
||||
]);
|
||||
getModelOptionsFromConfigs(configs, catalog, providerInfoByID).map(
|
||||
(option) => option.id,
|
||||
),
|
||||
).toEqual(["config-2", "config-1"]);
|
||||
});
|
||||
|
||||
it("drops configs whose ai_provider_id is absent from the provider map", () => {
|
||||
@@ -545,7 +535,9 @@ describe("getModelOptionsFromConfigs", () => {
|
||||
{ provider: "openai", available: true, models: [] },
|
||||
{ provider: "anthropic", available: true, models: [] },
|
||||
]);
|
||||
const partialMap = new Map<string, string>([["prov-openai", "openai"]]);
|
||||
const partialMap = new Map([
|
||||
["prov-openai", { provider: "openai", displayName: "OpenAI", icon: "" }],
|
||||
]);
|
||||
|
||||
expect(
|
||||
getModelOptionsFromConfigs(configs, catalog, partialMap).map(
|
||||
@@ -553,6 +545,98 @@ describe("getModelOptionsFromConfigs", () => {
|
||||
),
|
||||
).toEqual(["config-openai"]);
|
||||
});
|
||||
|
||||
it("preserves provider instance metadata for same-type providers", () => {
|
||||
const configs = [
|
||||
createConfig({
|
||||
id: "config-primary",
|
||||
ai_provider_id: "prov-anthropic-primary",
|
||||
model: "claude-sonnet-4-20250514",
|
||||
}),
|
||||
createConfig({
|
||||
id: "config-hyper",
|
||||
ai_provider_id: "prov-anthropic-hyper",
|
||||
model: "claude-opus-4-20250514",
|
||||
}),
|
||||
];
|
||||
const catalog = createCatalog([
|
||||
{ provider: "anthropic", available: true, models: [] },
|
||||
]);
|
||||
const sameTypeProviders = new Map([
|
||||
[
|
||||
"prov-anthropic-primary",
|
||||
{ provider: "anthropic", displayName: "Anthropic", icon: "" },
|
||||
],
|
||||
[
|
||||
"prov-anthropic-hyper",
|
||||
{
|
||||
provider: "anthropic",
|
||||
displayName: "Hyper",
|
||||
icon: "/icon/coder.svg",
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(
|
||||
getModelOptionsFromConfigs(configs, catalog, sameTypeProviders),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "config-primary",
|
||||
providerId: "prov-anthropic-primary",
|
||||
providerLabel: "Anthropic",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "config-hyper",
|
||||
providerId: "prov-anthropic-hyper",
|
||||
providerLabel: "Hyper",
|
||||
providerIcon: "/icon/coder.svg",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerInfoByIDFromConfigs", () => {
|
||||
it("maps ChatProviderConfig.id to provider metadata", () => {
|
||||
const map = providerInfoByIDFromConfigs([
|
||||
{
|
||||
...MockChatProviderConfig,
|
||||
id: "prov-openai",
|
||||
provider: "openai",
|
||||
display_name: "Primary OpenAI",
|
||||
icon: "/icon/openai.svg",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(map.get("prov-openai")).toEqual({
|
||||
provider: "openai",
|
||||
displayName: "Primary OpenAI",
|
||||
icon: "/icon/openai.svg",
|
||||
});
|
||||
expect(map.size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerInfoByIDFromUserConfigs", () => {
|
||||
it("maps UserChatProviderConfig.provider_id to provider metadata", () => {
|
||||
const map = providerInfoByIDFromUserConfigs([
|
||||
{
|
||||
provider_id: "prov-openai",
|
||||
provider: "openai",
|
||||
display_name: "Primary OpenAI",
|
||||
icon: "/icon/openai.svg",
|
||||
has_user_api_key: false,
|
||||
has_central_api_key_fallback: true,
|
||||
byok_enabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(map.get("prov-openai")).toEqual({
|
||||
provider: "openai",
|
||||
displayName: "Primary OpenAI",
|
||||
icon: "/icon/openai.svg",
|
||||
});
|
||||
expect(map.size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerTypeByIDFromConfigs", () => {
|
||||
@@ -584,6 +668,7 @@ describe("providerTypeByIDFromUserConfigs", () => {
|
||||
provider_id: "prov-openai",
|
||||
provider: "openai",
|
||||
display_name: "OpenAI",
|
||||
icon: "",
|
||||
has_user_api_key: false,
|
||||
has_central_api_key_fallback: true,
|
||||
byok_enabled: true,
|
||||
@@ -660,6 +745,7 @@ describe("resolveModelSelector", () => {
|
||||
provider_id: "prov-openai",
|
||||
provider: "openai",
|
||||
display_name: "OpenAI",
|
||||
icon: "",
|
||||
has_user_api_key: false,
|
||||
has_central_api_key_fallback: true,
|
||||
byok_enabled: true,
|
||||
@@ -693,6 +779,9 @@ describe("resolveModelSelector", () => {
|
||||
{
|
||||
id: "config-openai",
|
||||
provider: "openai",
|
||||
providerId: "prov-openai",
|
||||
providerLabel: "OpenAI",
|
||||
providerIcon: "",
|
||||
model: "gpt-4o",
|
||||
displayName: "GPT-4o",
|
||||
contextLimit: 128_000,
|
||||
|
||||
@@ -158,18 +158,55 @@ export const resolveModelOptionId = (
|
||||
return "";
|
||||
};
|
||||
|
||||
// providerTypeByIDFromConfigs and providerTypeByIDFromUserConfigs build
|
||||
// the ai_provider_id -> provider-type lookup that getModelOptionsFromConfigs
|
||||
// needs. The admin and user provider endpoints expose the provider id under
|
||||
// different field names (id vs provider_id), so each source has its own
|
||||
// helper to bake in the correct field and keep callers from mixing them up.
|
||||
export type ProviderInfo = {
|
||||
readonly provider: string;
|
||||
readonly displayName: string;
|
||||
readonly icon: string;
|
||||
};
|
||||
|
||||
// providerInfoByIDFromConfigs and providerInfoByIDFromUserConfigs build
|
||||
// the ai_provider_id -> provider metadata lookup that
|
||||
// getModelOptionsFromConfigs needs. The admin and user provider endpoints
|
||||
// expose the provider id under different field names (id vs provider_id), so
|
||||
// each source has its own helper to bake in the correct field.
|
||||
export const providerInfoByIDFromConfigs = (
|
||||
providerConfigs: readonly TypesGen.ChatProviderConfig[] | null | undefined,
|
||||
): ReadonlyMap<string, ProviderInfo> =>
|
||||
new Map(
|
||||
(providerConfigs ?? []).map((providerConfig) => [
|
||||
providerConfig.id,
|
||||
{
|
||||
provider: providerConfig.provider,
|
||||
displayName: providerConfig.display_name,
|
||||
icon: providerConfig.icon,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
export const providerInfoByIDFromUserConfigs = (
|
||||
providerConfigs:
|
||||
| readonly TypesGen.UserChatProviderConfig[]
|
||||
| null
|
||||
| undefined,
|
||||
): ReadonlyMap<string, ProviderInfo> =>
|
||||
new Map(
|
||||
(providerConfigs ?? []).map((providerConfig) => [
|
||||
providerConfig.provider_id,
|
||||
{
|
||||
provider: providerConfig.provider,
|
||||
displayName: providerConfig.display_name,
|
||||
icon: providerConfig.icon,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
export const providerTypeByIDFromConfigs = (
|
||||
providerConfigs: readonly TypesGen.ChatProviderConfig[] | null | undefined,
|
||||
): ReadonlyMap<string, string> =>
|
||||
new Map(
|
||||
(providerConfigs ?? []).map((providerConfig) => [
|
||||
providerConfig.id,
|
||||
providerConfig.provider,
|
||||
Array.from(providerInfoByIDFromConfigs(providerConfigs), ([id, info]) => [
|
||||
id,
|
||||
info.provider,
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -180,16 +217,16 @@ export const providerTypeByIDFromUserConfigs = (
|
||||
| undefined,
|
||||
): ReadonlyMap<string, string> =>
|
||||
new Map(
|
||||
(providerConfigs ?? []).map((providerConfig) => [
|
||||
providerConfig.provider_id,
|
||||
providerConfig.provider,
|
||||
]),
|
||||
Array.from(
|
||||
providerInfoByIDFromUserConfigs(providerConfigs),
|
||||
([id, info]) => [id, info.provider],
|
||||
),
|
||||
);
|
||||
|
||||
export const getModelOptionsFromConfigs = (
|
||||
configs: readonly TypesGen.ChatModelConfig[] | null | undefined,
|
||||
catalog: TypesGen.ChatModelsResponse | null | undefined,
|
||||
providerTypeByID: ReadonlyMap<string, string>,
|
||||
providerInfoByID: ReadonlyMap<string, ProviderInfo>,
|
||||
): readonly ModelSelectorOption[] => {
|
||||
if (!configs || !catalog) {
|
||||
return [];
|
||||
@@ -204,11 +241,10 @@ export const getModelOptionsFromConfigs = (
|
||||
}
|
||||
|
||||
const configID = config.id.trim();
|
||||
const provider = asString(providerTypeByID.get(config.ai_provider_id))
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const providerInfo = providerInfoByID.get(config.ai_provider_id);
|
||||
const provider = asString(providerInfo?.provider).trim().toLowerCase();
|
||||
const model = config.model.trim();
|
||||
if (!configID || !provider || !model) {
|
||||
if (!configID || !providerInfo || !provider || !model) {
|
||||
continue;
|
||||
}
|
||||
if (!availableProviders.has(provider)) {
|
||||
@@ -220,6 +256,9 @@ export const getModelOptionsFromConfigs = (
|
||||
options.push({
|
||||
id: configID,
|
||||
provider,
|
||||
providerId: config.ai_provider_id,
|
||||
providerLabel: providerInfo.displayName,
|
||||
providerIcon: providerInfo.icon,
|
||||
model,
|
||||
displayName,
|
||||
...(contextLimit !== undefined ? { contextLimit } : {}),
|
||||
@@ -227,7 +266,9 @@ export const getModelOptionsFromConfigs = (
|
||||
}
|
||||
|
||||
return options.sort((a, b) => {
|
||||
const providerCompare = a.provider.localeCompare(b.provider);
|
||||
const providerCompare = (a.providerLabel ?? a.provider).localeCompare(
|
||||
b.providerLabel ?? b.provider,
|
||||
);
|
||||
if (providerCompare !== 0) {
|
||||
return providerCompare;
|
||||
}
|
||||
@@ -264,7 +305,7 @@ export const resolveModelSelector = (
|
||||
options: getModelOptionsFromConfigs(
|
||||
modelConfigs.data,
|
||||
catalog.data,
|
||||
providerTypeByIDFromUserConfigs(userProviderConfigs.data),
|
||||
providerInfoByIDFromUserConfigs(userProviderConfigs.data),
|
||||
),
|
||||
isModelCatalogLoading:
|
||||
modelConfigs.isLoading ||
|
||||
|
||||
@@ -22,6 +22,7 @@ export const MockChatProviderConfig: ChatProviderConfig = {
|
||||
id: "provider-1",
|
||||
provider: "openai",
|
||||
display_name: "OpenAI",
|
||||
icon: "",
|
||||
enabled: true,
|
||||
has_api_key: true,
|
||||
central_api_key_enabled: true,
|
||||
|
||||
@@ -5547,6 +5547,7 @@ export const MockAIProviderOpenAI: TypesGen.AIProvider = {
|
||||
type: "openai",
|
||||
name: "openai",
|
||||
display_name: "OpenAI",
|
||||
icon: "",
|
||||
base_url: "https://api.openai.com",
|
||||
enabled: false,
|
||||
api_keys: [
|
||||
@@ -5566,6 +5567,7 @@ export const MockAIProviderAnthropic: TypesGen.AIProvider = {
|
||||
type: "anthropic",
|
||||
name: "anthropic",
|
||||
display_name: "Anthropic",
|
||||
icon: "",
|
||||
base_url: "https://api.anthropic.com",
|
||||
enabled: false,
|
||||
api_keys: [],
|
||||
@@ -5585,6 +5587,7 @@ export const MockAIProviderBedrock: TypesGen.AIProvider = {
|
||||
type: "bedrock",
|
||||
name: "bedrock",
|
||||
display_name: "Bedrock",
|
||||
icon: "",
|
||||
base_url: "https://bedrock-runtime.us-east-2.amazonaws.com",
|
||||
enabled: true,
|
||||
api_keys: [],
|
||||
@@ -5604,6 +5607,7 @@ export const MockAIProviderCopilot: TypesGen.AIProvider = {
|
||||
type: "copilot",
|
||||
name: "copilot",
|
||||
display_name: "GitHub Copilot",
|
||||
icon: "",
|
||||
base_url: "https://api.business.githubcopilot.com",
|
||||
enabled: true,
|
||||
api_keys: [],
|
||||
|
||||
Reference in New Issue
Block a user