mirror of
https://github.com/coder/coder.git
synced 2026-11-03 05:55:24 +08:00
feat!: scope chat model override settings to organizations (#28442)
Moves chat model override settings from deployment scope to organization scope, following the org-scoping of chat model configs in #28440. Depends on #28440 (based on `ethan/chat-models-availability-consolidation`). Remote dogfood UAT passed on this change, including multi-organization scenarios on a licensed deployment with real models. ## Problem Chat model configs are now owned by organizations, but the override settings that reference them stayed deployment-wide: admin subagent/title/compaction overrides in `site_configs`, personal overrides in `user_configs`, and the advisor model embedded in the advisor runtime JSON. A single deployment-level override can only point at one organization's model config, so overrides effectively worked for the default organization only, and title generation had to soften its hard-failure contract to tolerate unusable overrides. ## Fix Admin overrides (general, explore, title generation, compaction, advisor) and personal overrides (root, general, explore) become organization-scoped rows in two new typed tables, `chat_organization_model_overrides` and `chat_user_model_overrides`. Composite foreign keys on `(organization_id, model_config_id)` make cross-organization model references unrepresentable. The migration deletes the legacy serialized `site_configs`/`user_configs` override keys instead of migrating them; admins and users re-select models in the new organization-scoped settings. New endpoints live under `/api/experimental/organizations/{organization}/chats/model-overrides` (admin defaults) and `/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides` (personal overrides); the legacy deployment-level routes are removed. chatd resolves overrides via the chat's organization, restoring the hard-failure contract for configured-but-unusable title generation overrides. The deployment-level `AllowUsers` switch and advisor runtime limits stay deployment-scoped. The Coder Agents settings UI gains a per-organization "Defaults & overrides" page under AI Settings > Models, and the personal overrides page gains an organization selector for users in multiple organizations. Advisor telemetry now reports per-organization override rows. **BREAKING CHANGE**: Existing deployment-level overrides are dropped, not migrated. Every organization starts with no overrides after upgrade, and model selections must be re-applied in the new settings. The legacy deployment-level override endpoints are removed. Closes https://linear.app/coder/issue/CODAGT-872 > [!NOTE] > Xum (AI agent) authored this PR on behalf of @ibetitsmike. <!-- xum-attribution: model=claude-opus-4-6 thinking=high --> --------- Co-authored-by: Ethan Dickson <ethan@coder.com>
This commit is contained in:
co-authored by
Ethan Dickson
parent
3809314b31
commit
0ef331fcd0
@@ -6491,6 +6491,85 @@ func TestGetUserStatusCounts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChatOrganizationModelOverridesByContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
availableOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
availableModel := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
|
||||
Model: "bulk-available-" + uuid.NewString(),
|
||||
Enabled: true,
|
||||
OrganizationID: availableOrg.ID,
|
||||
})
|
||||
require.NoError(t, db.UpsertChatOrganizationModelOverride(ctx, database.UpsertChatOrganizationModelOverrideParams{
|
||||
OrganizationID: availableOrg.ID,
|
||||
Context: "general",
|
||||
ModelConfigID: availableModel.ID,
|
||||
}))
|
||||
|
||||
// An override referencing a disabled model reports the model unavailable.
|
||||
// dbgen coerces Enabled=false to true, so disable it after insertion.
|
||||
disabledOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
disabledModel := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
|
||||
Model: "bulk-disabled-" + uuid.NewString(),
|
||||
OrganizationID: disabledOrg.ID,
|
||||
})
|
||||
_, err := db.UpdateChatModelConfig(ctx, database.UpdateChatModelConfigParams{
|
||||
ID: disabledModel.ID,
|
||||
Model: disabledModel.Model,
|
||||
DisplayName: disabledModel.DisplayName,
|
||||
Enabled: false,
|
||||
IsDefault: disabledModel.IsDefault,
|
||||
ContextLimit: disabledModel.ContextLimit,
|
||||
CompressionThreshold: disabledModel.CompressionThreshold,
|
||||
Options: disabledModel.Options,
|
||||
AIProviderID: disabledModel.AIProviderID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.UpsertChatOrganizationModelOverride(ctx, database.UpsertChatOrganizationModelOverrideParams{
|
||||
OrganizationID: disabledOrg.ID,
|
||||
Context: "general",
|
||||
ModelConfigID: disabledModel.ID,
|
||||
}))
|
||||
|
||||
// Deleted organizations are excluded from the bulk read.
|
||||
deletedOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
deletedOrgModel := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
|
||||
Model: "bulk-deleted-org-" + uuid.NewString(),
|
||||
Enabled: true,
|
||||
OrganizationID: deletedOrg.ID,
|
||||
})
|
||||
require.NoError(t, db.UpsertChatOrganizationModelOverride(ctx, database.UpsertChatOrganizationModelOverrideParams{
|
||||
OrganizationID: deletedOrg.ID,
|
||||
Context: "general",
|
||||
ModelConfigID: deletedOrgModel.ID,
|
||||
}))
|
||||
require.NoError(t, db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{
|
||||
ID: deletedOrg.ID,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
}))
|
||||
|
||||
rows, err := db.GetChatOrganizationModelOverridesByContext(ctx, "general")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rows, 2)
|
||||
byOrg := make(map[uuid.UUID]database.GetChatOrganizationModelOverridesByContextRow, len(rows))
|
||||
for _, row := range rows {
|
||||
byOrg[row.OrganizationID] = row
|
||||
}
|
||||
available := byOrg[availableOrg.ID]
|
||||
require.True(t, available.ModelAvailable)
|
||||
require.Equal(t, availableModel.Model, available.Model)
|
||||
require.NotEmpty(t, available.ProviderType)
|
||||
unavailable := byOrg[disabledOrg.ID]
|
||||
require.False(t, unavailable.ModelAvailable)
|
||||
require.Empty(t, unavailable.Model)
|
||||
|
||||
otherContext, err := db.GetChatOrganizationModelOverridesByContext(ctx, "advisor")
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, otherContext)
|
||||
}
|
||||
|
||||
func TestOrganizationDeleteTrigger(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user