mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(coderd): allow deleting chat providers used in historical chats (#24568)
Drop the `chat_model_configs.provider -> chat_providers.provider` foreign key and soft-delete model configs when their provider is removed. The provider row is now hard-deleted inside a transaction that also tombstones its model configs and promotes a replacement default when needed. Historical chats and messages keep pointing at the soft-deleted model config rows, which are hidden from live/admin queries but still resolve for read. The runtime chat path already falls back to the default model config when a soft-deleted config is looked up. Replaces the lost FK validation in the create/update model-config handlers with an explicit provider lookup that returns the existing `Chat provider is not configured.` 400. ## UX **Admin deleting a chat provider that has historical usage** - Before: blocked with 400 `Provider models are still referenced by existing chats.` Admins had no in-product way to remove a provider that had ever been used. - After: delete succeeds (204). Any model configs under that provider are soft-deleted. If the removed provider owned the default model config, one of the remaining live configs is auto-promoted to the new default. The promotion is deterministic (`ensureDefaultChatModelConfig` picks the first live config by `provider ASC, model ASC, updated_at DESC, id DESC`); there is no picker, and no toast or response detail names which config became the new default. **End users with chats that used a deleted provider's model** - Old chats still open and their history still renders unchanged. - Sending a new turn in such a chat silently falls back to the current default model. No banner or warning tells the user the original model is gone. - The model picker no longer lists the deleted model. - If no default model config exists at all after the delete, sending a new turn fails with `no default chat model config is available`. **Admin creating or updating a model config against a provider that is not configured** - Same as before: 400 `Chat provider is not configured.` Only the detection mechanism changed (explicit `FOR UPDATE` lookup inside the transaction, which also serializes against a concurrent provider delete). **Admin updating a model config whose row disappears mid-transaction** - Now returns the standard 404 `Resource not found or you do not have access to this resource` instead of the previous 500 that leaked `sql: no rows in result set` in the detail. Unrelated internal races (for example a race on the promoted default candidate) are still reported as 500 so they are not misclassified as "your target is gone". Closes CODAGT-23
This commit is contained in:
@@ -4283,6 +4283,23 @@ func (q *sqlQuerier) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteChatModelConfigsByProvider = `-- name: DeleteChatModelConfigsByProvider :exec
|
||||
UPDATE
|
||||
chat_model_configs
|
||||
SET
|
||||
deleted = TRUE,
|
||||
deleted_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
provider = $1::text
|
||||
AND deleted = FALSE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteChatModelConfigsByProvider(ctx context.Context, provider string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteChatModelConfigsByProvider, provider)
|
||||
return err
|
||||
}
|
||||
|
||||
const getChatModelConfigByID = `-- name: GetChatModelConfigByID :one
|
||||
SELECT
|
||||
id, provider, model, display_name, created_by, updated_by, enabled, is_default, deleted, deleted_at, created_at, updated_at, context_limit, compression_threshold, options
|
||||
@@ -4699,6 +4716,37 @@ func (q *sqlQuerier) GetChatProviderByID(ctx context.Context, id uuid.UUID) (Cha
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatProviderByIDForUpdate = `-- name: GetChatProviderByIDForUpdate :one
|
||||
SELECT
|
||||
id, provider, display_name, api_key, api_key_key_id, created_by, enabled, created_at, updated_at, base_url, central_api_key_enabled, allow_user_api_key, allow_central_api_key_fallback
|
||||
FROM
|
||||
chat_providers
|
||||
WHERE
|
||||
id = $1::uuid
|
||||
FOR UPDATE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetChatProviderByIDForUpdate(ctx context.Context, id uuid.UUID) (ChatProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, getChatProviderByIDForUpdate, id)
|
||||
var i ChatProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Provider,
|
||||
&i.DisplayName,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedBy,
|
||||
&i.Enabled,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.BaseUrl,
|
||||
&i.CentralApiKeyEnabled,
|
||||
&i.AllowUserApiKey,
|
||||
&i.AllowCentralApiKeyFallback,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatProviderByProvider = `-- name: GetChatProviderByProvider :one
|
||||
SELECT
|
||||
id, provider, display_name, api_key, api_key_key_id, created_by, enabled, created_at, updated_at, base_url, central_api_key_enabled, allow_user_api_key, allow_central_api_key_fallback
|
||||
@@ -4729,6 +4777,37 @@ func (q *sqlQuerier) GetChatProviderByProvider(ctx context.Context, provider str
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatProviderByProviderForUpdate = `-- name: GetChatProviderByProviderForUpdate :one
|
||||
SELECT
|
||||
id, provider, display_name, api_key, api_key_key_id, created_by, enabled, created_at, updated_at, base_url, central_api_key_enabled, allow_user_api_key, allow_central_api_key_fallback
|
||||
FROM
|
||||
chat_providers
|
||||
WHERE
|
||||
provider = $1::text
|
||||
FOR UPDATE
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetChatProviderByProviderForUpdate(ctx context.Context, provider string) (ChatProvider, error) {
|
||||
row := q.db.QueryRowContext(ctx, getChatProviderByProviderForUpdate, provider)
|
||||
var i ChatProvider
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Provider,
|
||||
&i.DisplayName,
|
||||
&i.APIKey,
|
||||
&i.ApiKeyKeyID,
|
||||
&i.CreatedBy,
|
||||
&i.Enabled,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.BaseUrl,
|
||||
&i.CentralApiKeyEnabled,
|
||||
&i.AllowUserApiKey,
|
||||
&i.AllowCentralApiKeyFallback,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getChatProviders = `-- name: GetChatProviders :many
|
||||
SELECT
|
||||
id, provider, display_name, api_key, api_key_key_id, created_by, enabled, created_at, updated_at, base_url, central_api_key_enabled, allow_user_api_key, allow_central_api_key_fallback
|
||||
|
||||
Reference in New Issue
Block a user