mirror of
https://github.com/coder/coder.git
synced 2026-11-01 04:47:17 +08:00
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>
104 lines
3.3 KiB
Go
104 lines
3.3 KiB
Go
package chatd
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
|
|
)
|
|
|
|
const compactionOverrideContext = "compaction"
|
|
|
|
func readCompactionModelOverride(
|
|
ctx context.Context,
|
|
db database.Store,
|
|
organizationID uuid.UUID,
|
|
) (database.ChatOrganizationModelOverride, error) {
|
|
//nolint:gocritic // Chatd is internal, not a user, so this read uses AsChatd.
|
|
chatdCtx := dbauthz.AsChatd(ctx)
|
|
override, err := db.GetChatOrganizationModelOverride(chatdCtx, database.GetChatOrganizationModelOverrideParams{
|
|
OrganizationID: organizationID,
|
|
Context: compactionOverrideContext,
|
|
})
|
|
if err != nil {
|
|
return database.ChatOrganizationModelOverride{}, xerrors.Errorf(
|
|
"get chat compaction model override: %w",
|
|
err,
|
|
)
|
|
}
|
|
return override, nil
|
|
}
|
|
|
|
// resolvedCompactionOverride is the compaction override resolved at
|
|
// prepare time. The provider/model identity is resolved without building
|
|
// the model client so metrics recorded before the client exists
|
|
// (still-over-limit) attribute to the same model as the compact action's.
|
|
type resolvedCompactionOverride struct {
|
|
Config database.ChatModelConfig
|
|
// ReasoningEffort is the override's requested effort, passed to the
|
|
// model-call resolver when the compact action builds the client.
|
|
ReasoningEffort *string
|
|
ResolvedProvider string
|
|
ResolvedModel string
|
|
}
|
|
|
|
// resolveCompactionOverrideConfig resolves the stored organization-scoped
|
|
// compaction model override. Unset, stale, and credential-less
|
|
// overrides fall back to the chat model (nil override). This runs on every
|
|
// generation prepare because the override's context limit feeds the
|
|
// compaction trigger; the model client is built only when compaction runs.
|
|
func (p *Server) resolveCompactionOverrideConfig(
|
|
ctx context.Context,
|
|
chat database.Chat,
|
|
) (*resolvedCompactionOverride, error) {
|
|
override, err := readCompactionModelOverride(ctx, p.db, chat.OrganizationID)
|
|
if err != nil {
|
|
if xerrors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil //nolint:nilnil // No row means use the chat model.
|
|
}
|
|
return nil, xerrors.Errorf(
|
|
"read compaction model override: %w",
|
|
err,
|
|
)
|
|
}
|
|
|
|
modelConfig, providerName, overrideEffort, overrideSet, err := p.resolveOrganizationModelOverride(
|
|
ctx,
|
|
compactionOverrideContext,
|
|
override,
|
|
chat.OwnerID,
|
|
func(ctx context.Context, modelConfigID uuid.UUID) (database.ChatModelConfig, string, error) {
|
|
return p.resolveModelConfigAndNormalizedProvider(ctx, chat.OwnerID, modelConfigID)
|
|
},
|
|
func(ctx context.Context, ownerID uuid.UUID, aiProviderID uuid.UUID) (chatprovider.ProviderAPIKeys, error) {
|
|
return p.resolveUserProviderAPIKeys(ctx, ownerID, aiProviderID)
|
|
},
|
|
modelOverrideFailureModeSoft,
|
|
)
|
|
if err != nil || !overrideSet {
|
|
return nil, err
|
|
}
|
|
// Already validated by the shared resolver; failure is unreachable.
|
|
resolvedProvider, resolvedModel, err := chatprovider.ResolveModelWithProviderHint(
|
|
modelConfig.Model,
|
|
providerName,
|
|
)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf(
|
|
"resolve compaction model override identity: %w",
|
|
err,
|
|
)
|
|
}
|
|
return &resolvedCompactionOverride{
|
|
Config: modelConfig,
|
|
ReasoningEffort: overrideEffort,
|
|
ResolvedProvider: resolvedProvider,
|
|
ResolvedModel: resolvedModel,
|
|
}, nil
|
|
}
|