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:
Michael Suchacz
2026-08-24 11:20:26 +01:00
committed by GitHub
co-authored by Ethan Dickson
parent 3809314b31
commit 0ef331fcd0
75 changed files with 4800 additions and 4952 deletions
+211
View File
@@ -4087,6 +4087,58 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `reasoning` | [codersdk.ChatModelReasoningOptions](#codersdkchatmodelreasoningoptions) | false | | |
| `user` | string | false | | |
## codersdk.ChatModelOverrideContext
```json
"general"
```
### Properties
#### Enumerated Values
| Value(s) |
|-------------------------------------------------------------------|
| `advisor`, `compaction`, `explore`, `general`, `title_generation` |
## codersdk.ChatModelOverrideResponse
```json
{
"context": "general",
"model_config_id": "string",
"reasoning_effort": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------|------------------------------------------------------------------------|----------|--------------|-------------|
| `context` | [codersdk.ChatModelOverrideContext](#codersdkchatmodeloverridecontext) | false | | |
| `model_config_id` | string | false | | |
| `reasoning_effort` | string | false | | |
## codersdk.ChatModelOverridesResponse
```json
{
"overrides": [
{
"context": "general",
"model_config_id": "string",
"reasoning_effort": "string"
}
]
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-------------|-----------------------------------------------------------------------------------|----------|--------------|-------------|
| `overrides` | array of [codersdk.ChatModelOverrideResponse](#codersdkchatmodeloverrideresponse) | false | | |
## codersdk.ChatModelProviderDescriptor
```json
@@ -4390,6 +4442,80 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `top_logprobs` | integer | false | | |
| `user` | string | false | | |
## codersdk.ChatPersonalModelOverride
```json
{
"context": "root",
"is_set": true,
"mode": "deployment_default",
"model_config_id": "string",
"reasoning_effort": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------|----------------------------------------------------------------------------------------|----------|--------------|-------------|
| `context` | [codersdk.ChatPersonalModelOverrideContext](#codersdkchatpersonalmodeloverridecontext) | false | | |
| `is_set` | boolean | false | | |
| `mode` | [codersdk.ChatPersonalModelOverrideMode](#codersdkchatpersonalmodeloverridemode) | false | | |
| `model_config_id` | string | false | | |
| `reasoning_effort` | string | false | | |
## codersdk.ChatPersonalModelOverrideContext
```json
"root"
```
### Properties
#### Enumerated Values
| Value(s) |
|------------------------------|
| `explore`, `general`, `root` |
## codersdk.ChatPersonalModelOverrideDeploymentDefaults
```json
{
"explore": {
"context": "general",
"model_config_id": "string",
"reasoning_effort": "string"
},
"general": {
"context": "general",
"model_config_id": "string",
"reasoning_effort": "string"
}
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-----------|--------------------------------------------------------------------------|----------|--------------|-------------|
| `explore` | [codersdk.ChatModelOverrideResponse](#codersdkchatmodeloverrideresponse) | false | | |
| `general` | [codersdk.ChatModelOverrideResponse](#codersdkchatmodeloverrideresponse) | false | | |
## codersdk.ChatPersonalModelOverrideMode
```json
"deployment_default"
```
### Properties
#### Enumerated Values
| Value(s) |
|-----------------------------------------------|
| `chat_default`, `deployment_default`, `model` |
## codersdk.ChatPlanMode
```json
@@ -14982,6 +15108,22 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
| `user_roles` | object | false | | |
| » `[any property]` | [codersdk.ChatRole](#codersdkchatrole) | false | | |
## codersdk.UpdateChatModelOverrideRequest
```json
{
"model_config_id": "string",
"reasoning_effort": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------|--------|----------|--------------|-------------|
| `model_config_id` | string | false | | |
| `reasoning_effort` | string | false | | |
## codersdk.UpdateChatModelRequest
```json
@@ -15485,6 +15627,24 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
| `theme_light` | `dark`, `dark-protan-deuter`, `dark-tritan`, `light`, `light-protan-deuter`, `light-tritan` |
| `theme_mode` | `single`, `sync` |
## codersdk.UpdateUserChatPersonalModelOverrideRequest
```json
{
"mode": "deployment_default",
"model_config_id": "string",
"reasoning_effort": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|--------------------|----------------------------------------------------------------------------------|----------|--------------|-------------|
| `mode` | [codersdk.ChatPersonalModelOverrideMode](#codersdkchatpersonalmodeloverridemode) | false | | |
| `model_config_id` | string | false | | |
| `reasoning_effort` | string | false | | |
## codersdk.UpdateUserNotificationPreferences
```json
@@ -16111,6 +16271,57 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| `theme_mode` | [codersdk.ThemeMode](#codersdkthememode) | false | | |
| `theme_preference` | string | false | | Theme preference is the legacy single-field appearance setting. In "single" mode it mirrors the active theme. In "sync" mode modern clients normally mirror the active OS slot, but older clients can update only this field, so it may diverge from ThemeLight or ThemeDark until a modern client saves the full appearance state again. |
## codersdk.UserChatPersonalModelOverridesResponse
```json
{
"deployment_defaults": {
"explore": {
"context": "general",
"model_config_id": "string",
"reasoning_effort": "string"
},
"general": {
"context": "general",
"model_config_id": "string",
"reasoning_effort": "string"
}
},
"enabled": true,
"explore": {
"context": "root",
"is_set": true,
"mode": "deployment_default",
"model_config_id": "string",
"reasoning_effort": "string"
},
"general": {
"context": "root",
"is_set": true,
"mode": "deployment_default",
"model_config_id": "string",
"reasoning_effort": "string"
},
"root": {
"context": "root",
"is_set": true,
"mode": "deployment_default",
"model_config_id": "string",
"reasoning_effort": "string"
}
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
|-----------------------|--------------------------------------------------------------------------------------------------------------|----------|--------------|-------------|
| `deployment_defaults` | [codersdk.ChatPersonalModelOverrideDeploymentDefaults](#codersdkchatpersonalmodeloverridedeploymentdefaults) | false | | |
| `enabled` | boolean | false | | |
| `explore` | [codersdk.ChatPersonalModelOverride](#codersdkchatpersonalmodeloverride) | false | | |
| `general` | [codersdk.ChatPersonalModelOverride](#codersdkchatpersonalmodeloverride) | false | | |
| `root` | [codersdk.ChatPersonalModelOverride](#codersdkchatpersonalmodeloverride) | false | | |
## codersdk.UserLatency
```json