refactor!: drop chat_model_configs provider column - #26877
Conversation
|
/coder-agents-review |
|
Chat: Review posted | View chat Review historydeep-review v0.9.0 | Round 2 | Last posted: Round 2, 11 findings (1 P2, 7 P3, 3 Nit), APPROVE. Review Finding inventoryFindings
Contested and acknowledged(none) Round logRound 1Panel: Netero + 16 reviewers. 1 P2, 4 P3, 3 Nit. Reviewed against 7179be2..7c4d325. Round 2Churn guard: PROCEED. 11/11 findings addressed. Netero: no findings. Law: don't split. Panel (Bisky, Mafuuu, Mafu-san, Nami, Kite): no new findings. All R1 fixes verified. Reviewed against 6b3341a..6de8ea0. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
Pull request overview
This PR removes the denormalized chat_model_configs.provider column and migrates all backend, SDK, and frontend surfaces to treat ai_providers.type (reachable via chat_model_configs.ai_provider_id) as the single source of truth for provider identity. It also keeps telemetry emitting a provider string by deriving it from ai_providers.type at query time.
Changes:
- Drops
chat_model_configs.provider(and related indexes) via migration000534, and updates SQL queries/code to derive provider type fromai_providers. - Updates experimental API/SDK
ChatModelConfigto removeproviderand requireai_provider_idin responses; updates Go + TS generated types and all call sites. - Updates the web UI to resolve provider type through an
ai_provider_id -> provider typemap (from owner-only or user-visible provider endpoints, depending on audience).
Reviewed changes
Copilot reviewed 86 out of 90 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| site/src/testHelpers/chatModels.ts | Updates model-config test helper to use ai_provider_id instead of provider. |
| site/src/pages/AISettingsPage/ModelsPage/testFixtures.ts | Removes provider from model fixtures and relies on ai_provider_id. |
| site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx | Plumbs providerTypeByID through the models page view. |
| site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx | Updates story args to include a providerTypeByID map. |
| site/src/pages/AISettingsPage/ModelsPage/ModelsPage.tsx | Builds providerTypeByID from provider configs and sorts models using it. |
| site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx | Resolves provider icon input via providerTypeByID + ai_provider_id. |
| site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.tsx | Stops sending provider in create/update requests; uses ai_provider_id. |
| site/src/pages/AISettingsPage/CoderAgentsPage/components/SubagentModelOverrideSettings.tsx | Resolves model option provider via providerTypeByID + ai_provider_id. |
| site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx | Plumbs providerTypeByID through Coder Agents settings view. |
| site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.stories.tsx | Updates stories to use ai_provider_id and a providerTypeByID map. |
| site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx | Loads provider configs and builds providerTypeByID for settings pages. |
| site/src/pages/AgentsPage/utils/modelOptions.ts | Updates model option derivation to use ai_provider_id + providerTypeByID. |
| site/src/pages/AgentsPage/utils/modelOptions.test.ts | Updates tests for new getModelOptionsFromConfigs signature and behavior. |
| site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.tsx | Resolves provider icon via providerTypeByID instead of modelConfig.provider. |
| site/src/pages/AgentsPage/components/UserCompactionThresholdSettings.stories.tsx | Updates stories to set ai_provider_id and pass providerTypeByID. |
| site/src/pages/AgentsPage/components/ChatsSidebar/tree/modelDisplayName.ts | Removes legacy provider:model fallback logic; uses config UUID resolution. |
| site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.test.tsx | Removes test coverage for legacy provider:model sidebar fallback. |
| site/src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx | Updates stories to set ai_provider_id instead of provider. |
| site/src/pages/AgentsPage/components/ChatConversation/chatHelpers.ts | Simplifies model resolution logic (exact id match only). |
| site/src/pages/AgentsPage/components/ChatConversation/chatHelpers.test.ts | Updates tests to match the simplified resolution behavior. |
| site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx | Updates story model configs to set ai_provider_id. |
| site/src/pages/AgentsPage/components/AdvisorSettings.stories.tsx | Updates story model configs to set ai_provider_id. |
| site/src/pages/AgentsPage/AgentsPageView.stories.tsx | Updates stories/mocks for ChatModelConfig shape and provider key configs. |
| site/src/pages/AgentsPage/AgentsPage.tsx | Uses user-visible provider configs to build provider-type map for model options. |
| site/src/pages/AgentsPage/AgentSettingsUserAgentsPageView.stories.tsx | Updates stories to set ai_provider_id and explicit providers in options. |
| site/src/pages/AgentsPage/AgentSettingsUserAgentsPage.tsx | Loads user-visible provider configs and passes providerTypeByID to option builder. |
| site/src/pages/AgentsPage/AgentSettingsCompactionPageView.tsx | Adds providerTypeByID prop pass-through for compaction settings. |
| site/src/pages/AgentsPage/AgentSettingsCompactionPageView.stories.tsx | Updates story args for required ai_provider_id and providerTypeByID. |
| site/src/pages/AgentsPage/AgentSettingsCompactionPage.tsx | Loads user-visible provider configs and builds providerTypeByID. |
| site/src/pages/AgentsPage/AgentSettingsAPIKeysPageView.tsx | Removes legacy fallback matching based on provider string. |
| site/src/pages/AgentsPage/AgentSettingsAPIKeysPage.stories.tsx | Updates story models to use ai_provider_id. |
| site/src/pages/AgentsPage/AgentCreatePage.tsx | Uses user-visible provider configs to build provider-type map for model options. |
| site/src/pages/AgentsPage/AgentChatPage.tsx | Uses user-visible provider configs to build provider-type map for model options. |
| site/src/pages/AgentsPage/AgentChatPage.stories.tsx | Updates story mocks for user provider key configs. |
| site/src/modules/aiModels/providerStates.ts | Removes provider-string fallbacks and keys provider state strictly by ai_provider_id. |
| site/src/modules/aiModels/providerStates.test.ts | Updates provider state tests for ai_provider_id-only behavior. |
| site/src/api/typesGenerated.ts | Updates generated TS types: remove provider, require ai_provider_id in responses. |
| scaletest/chat/provider.go | Updates scaletest client logic for non-pointer AIProviderID. |
| enterprise/coderd/exp_chats_test.go | Updates enterprise tests to stop sending/expecting provider in requests/responses. |
| codersdk/chats.go | Removes provider field from SDK model config and requests; makes AIProviderID required in response model. |
| coderd/x/chatd/turn_summary_internal_test.go | Updates insert params for removed Provider field. |
| coderd/x/chatd/title_override_internal_test.go | Updates mocks and test data for provider resolution via ai_provider_id. |
| coderd/x/chatd/tasks_test.go | Updates test seed to not rely on config Provider. |
| coderd/x/chatd/subagent.go | Removes now-dead fallback branch when AIProviderID is missing, returning sql.ErrNoRows. |
| coderd/x/chatd/subagent_internal_test.go | Updates internal tests and seeds to link model configs to the intended provider rows. |
| coderd/x/chatd/quickgen.go | Updates title-generation path to get provider type from resolved route/provider row. |
| coderd/x/chatd/quickgen_internal_test.go | Updates tests for changed query return type and config/provider handling. |
| coderd/x/chatd/model_routing_internal_test.go | Updates routing tests to not rely on stored config Provider. |
| coderd/x/chatd/model_routing_direct.go | Returns sql.ErrNoRows when AIProviderID is missing instead of using removed column. |
| coderd/x/chatd/integration_test.go | Removes sending Provider in config creation requests. |
| coderd/x/chatd/helpers_test.go | Updates test seed to not set config Provider. |
| coderd/x/chatd/generation_preparer.go | Resolves provider type via modelRoute.providerHint() for file prompt conversion. |
| coderd/x/chatd/generation_preparer_internal_test.go | Updates test seeds for removed config Provider. |
| coderd/x/chatd/configcache_internal_test.go | Updates test model config helper for removed Provider. |
| coderd/x/chatd/chatstate/trigger_test.go | Updates test seed for removed config Provider. |
| coderd/x/chatd/chatstate/machine_test.go | Updates test seed for removed config Provider. |
| coderd/x/chatd/chatstate/family_test.go | Updates test seed for removed config Provider. |
| coderd/x/chatd/chatprompt/chatprompt_test.go | Updates test to link model config to provider row via ai_provider_id. |
| coderd/x/chatd/chatdebug/service_test.go | Updates debug provenance assertions to derive provider type from provider row. |
| coderd/x/chatd/chatd.go | Updates debug-run provider provenance wiring away from config Provider. |
| coderd/x/chatd/chatd_test.go | Updates many test helpers to link model configs via ai_provider_id. |
| coderd/x/chatd/chatd_retry_test.go | Updates retry tests for removed config Provider. |
| coderd/x/chatd/chatd_internal_test.go | Updates mocks to include GetAIProviderByID / GetAIProviderKeysByProviderID expectations. |
| coderd/x/chatd/chatd_chainmode_test.go | Updates chainmode tests for removed config Provider. |
| coderd/x/chatd/advisor_internal_test.go | Updates advisor override tests to resolve provider type via ai_provider_id. |
| coderd/workspaceagents_active_chat_internal_test.go | Updates seed helper to not set config Provider. |
| coderd/telemetry/telemetry_test.go | Updates telemetry tests to link model configs via ai_provider_id. |
| coderd/exp_chats.go | Removes provider-string handling in create/update/list paths; assumes ai_provider_id invariant for active configs. |
| coderd/exp_chats_test.go | Updates API tests to no longer use/expect config Provider. |
| coderd/database/queries/chats.sql | Derives provider strings via joins for cost breakdown + telemetry snapshot. |
| coderd/database/queries/chatmodelconfigs.sql | Removes provider column usage; introduces GetEnabledChatModelConfigs row type with derived provider. |
| coderd/database/queries.sql.go | Regenerates SQLC output to reflect schema/query changes and new row types. |
| coderd/database/querier.go | Updates querier interface for new GetEnabledChatModelConfigs return type; removes deleted queries. |
| coderd/database/querier_test.go | Updates tests to match new insert helper signature and new enabled-config row type. |
| coderd/database/models.go | Removes Provider from database.ChatModelConfig. |
| coderd/database/migrations/000534_drop_chat_model_configs_provider.up.sql | Drops provider indexes and column. |
| coderd/database/migrations/000534_drop_chat_model_configs_provider.down.sql | Restores provider column/indexes and backfills from ai_providers.type. |
| coderd/database/dump.sql | Updates canonical schema dump: removes chat_model_configs.provider and its indexes. |
| coderd/database/dbpurge/dbpurge_test.go | Updates purge tests for removed config Provider. |
| coderd/database/dbmock/dbmock.go | Regenerates mocks for removed queries + updated return types. |
| coderd/database/dbmetrics/querymetrics.go | Removes metrics wrappers for deleted DB methods; updates enabled-configs return type. |
| coderd/database/dbgen/dbgen.go | Updates DB test generator to default/link an OpenAI provider via ai_provider_id. |
| coderd/database/dbgen/dbgen_test.go | Updates assertions to check provider type via provider row, not config column. |
| coderd/database/dbauthz/dbauthz.go | Removes authorization wrappers for deleted DB methods; updates enabled-configs return type. |
| coderd/database/dbauthz/dbauthz_test.go | Updates authorization tests for removed methods and updated insert/update params. |
| coderd/coderdtest/chat.go | Removes sending Provider in test helper for creating model configs. |
| coderd/ai_providers_backfill.go | Deletes startup backfill for syncing provider strings; keeps Bedrock provider-type promotion backfill. |
| coderd/ai_providers_backfill_test.go | Removes tests that covered the deleted provider-string backfill. |
| cli/server.go | Stops invoking the deleted startup backfill. |
| cli/exp_scaletest_chat_test.go | Updates scaletest CLI for non-pointer AIProviderID. |
Files not reviewed (4)
- coderd/database/dbmetrics/querymetrics.go: Generated file
- coderd/database/dbmock/dbmock.go: Generated file
- coderd/database/models.go: Generated file
- coderd/database/querier.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
7c4d325 to
ded1375
Compare
There was a problem hiding this comment.
Clean decomposition of a single logical change across schema, backend, SDK, frontend, and telemetry. The invariant chain (CHECK constraint guaranteeing ai_provider_id for active rows, FK without CASCADE/SET NULL, soft-delete semantics) is verified and documented at every load-bearing site. Telemetry INNER JOIN vs cost LEFT JOIN distinction is correctly designed for their respective filter contexts. Test density at 67.3% is strong.
"I tried to build a case against this change and couldn't." (Pariston)
Severity count: 1 P2, 4 P3, 3 Nit.
The P2 is a frontend loading race: the new dependency on userChatProviderConfigs() is not reflected in the loading guard, so users see a flash of "No Models Available" on cold loads until the provider query settles. Five reviewers independently converged on the debug-run empty-provider finding (P3), which loses the provider dimension in diagnostic records when route resolution fails. Four comments repeat a pattern of restating the code around otherwise correct invariant documentation (P3).
🤖 This review was automatically generated with Coder Agents.
johnstcn
left a comment
There was a problem hiding this comment.
partial review, will continue later
| // Must run after BackfillBedrockProviderType; shares aibridgeInitCtx so | ||
| // a timeout on the first backfill will skip this one until next startup. | ||
| coderd.BackfillChatModelConfigProviderStrings(aibridgeInitCtx, options.Database, logger.Named("aibridge.backfill")) |
There was a problem hiding this comment.
reviewer note: added in v2.34.0
There was a problem hiding this comment.
No change needed. BackfillChatModelConfigProviderStrings (v2.34.0) only repaired the chat_model_configs.provider text column that this PR drops. Migration 000534 just DROP COLUMN provider and never reads it; ai_provider_id is populated independently. So an upgrade that skips v2.34 is harmless: the data the backfill repaired is being removed.
🤖 Posted using
/amend-reviewskill via Coder Agents.
| rowA := testutil.Fake(s.T(), faker, database.GetEnabledChatModelConfigsRow{}) | ||
| rowB := testutil.Fake(s.T(), faker, database.GetEnabledChatModelConfigsRow{}) |
There was a problem hiding this comment.
nit: Why the naming change from config[AB] to row[AB]?
There was a problem hiding this comment.
Kept rowA/rowB: the return type changed to GetEnabledChatModelConfigsRow (the embedded config plus the derived provider), so row is accurate and config would mislead.
🤖 Posted using
/amend-reviewskill via Coder Agents.
| func reuseOrCreateAIProviderOfType(t *testing.T, db database.Store, providerType string) uuid.UUID { | ||
| t.Helper() | ||
| providers, err := db.GetAIProviders(context.Background(), database.GetAIProvidersParams{IncludeDisabled: true}) | ||
| require.NoError(t, err) | ||
| var provider database.AIProvider | ||
| for _, candidate := range providers { | ||
| if candidate.Type != database.AIProviderType(providerType) { | ||
| continue | ||
| } | ||
| if provider.ID == uuid.Nil || candidate.CreatedAt.After(provider.CreatedAt) { | ||
| provider = candidate | ||
| } | ||
| } | ||
| if provider.ID == uuid.Nil { | ||
| provider = dbgen.AIProvider(t, db, database.AIProvider{ | ||
| Type: database.AIProviderType(providerType), | ||
| }) | ||
| } | ||
| return provider.ID | ||
| } |
There was a problem hiding this comment.
Shouldn't we instead ensure that the test fixtures are created as expected?
Also, this is only used once.
There was a problem hiding this comment.
Inlined the single-use helper into its caller (and did the same for the sibling reuseOrCreateInternalAIProvider), addressing the used-once point. I kept the reuse-newest-of-type behavior rather than threading a provider ID from the seed helper: the test must link the config to the provider seeded earlier in the test (which carries the mock base URL and key), and explicit threading through the call sites is churn with no behavioral gain.
🤖 Posted using
/amend-reviewskill via Coder Agents.
ded1375 to
40cf684
Compare
johnstcn
left a comment
There was a problem hiding this comment.
Will review once more when rebased, but I don't see anything blocking here 👍
Provider type already lives authoritatively in ai_providers.type, reachable on every active row through ai_provider_id, which the chat_model_configs_ai_provider_required_when_active CHECK makes mandatory. The stored provider string was a denormalized duplicate kept in sync by a startup backfill that the system no longer needs. Every read site now derives the provider type from the linked ai_providers row. The routing fallbacks that read the stored string sit behind an !ai_provider_id branch that is unreachable for active rows, so they return sql.ErrNoRows instead. Telemetry keeps emitting provider, now sourced from ai_providers.type via JOIN, so the BigQuery column and Nexus dashboards keep working. The experimental API drops provider and makes ai_provider_id required, since those endpoints only return active configs. Ships in a single release: production reads the column via SELECT *, so operators must scale to zero before upgrading. There is no rollback. CODAGT-599
40cf684 to
6de8ea0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 88 out of 92 changed files in this pull request and generated no new comments.
Files not reviewed (4)
- coderd/database/dbmetrics/querymetrics.go: Generated file
- coderd/database/dbmock/dbmock.go: Generated file
- coderd/database/models.go: Generated file
- coderd/database/querier.go: Generated file
|
/coder-agents-review |
There was a problem hiding this comment.
All 11 R1 findings addressed with root-cause fixes, not patches. No new findings from the R2 panel (Bisky, Mafuuu, Mafu-san, Nami, Kite) or from Netero.
The fixes are well-structured. CRF-2 (loading race) was fixed with a useModelOptions hook that folds three query loading states into one derived source, making the race structurally impossible for consumers rather than guarding it per page. CRF-3 (debug-run empty provider) was fixed with a enabledAIProviderByID fallback when route resolution fails, covered by a test that forces the exact failure path. CRF-11 (inline map construction) was fixed with providerTypeByIDFromConfigs and providerTypeByIDFromUserConfigs helpers that bake in the correct source field per audience. Comment trims (CRF-5 through CRF-9) land at the right level: invariant naming without mechanism narration.
Test density rose from 67.3% to 72.6%. New tests exercise the exact gaps R1 identified: the provider-map-miss drop behavior, the pending-provider-query loading race, and the route-failure provider-derivation fallback.
"Every stone is real." (Bisky)
🤖 This review was automatically generated with Coder Agents.
11c116b to
fda47e4
Compare
DanielleMaywood
left a comment
There was a problem hiding this comment.
Just a quick frontend review. As most of it is prop drilling not much to comment on
| /** | ||
| * useModelOptions owns the three queries the user-facing model selector needs | ||
| * (model configs, the model catalog, and the user provider configs) and | ||
| * derives its own loading flag. | ||
| * | ||
| * Provider identity lives in a separate query (userChatProviderConfigs), so a | ||
| * page that renders with configs loaded but that query still pending would | ||
| * build an empty provider map, drop every option, and flash "No Models". By | ||
| * folding all three loading states into a single derived source, that race is | ||
| * structurally impossible for every consumer instead of being guarded per page. | ||
| */ | ||
| export const useModelOptions = (): UseModelOptionsResult => { | ||
| const modelConfigsQuery = useQuery(chatModelConfigs()); | ||
| const catalogQuery = useQuery(chatModels()); | ||
| const providerConfigsQuery = useQuery(userChatProviderConfigs()); | ||
|
|
||
| const options = getModelOptionsFromConfigs( | ||
| modelConfigsQuery.data, | ||
| catalogQuery.data, | ||
| providerTypeByIDFromUserConfigs(providerConfigsQuery.data), | ||
| ); | ||
|
|
||
| return { | ||
| options, | ||
| isModelCatalogLoading: | ||
| modelConfigsQuery.isLoading || | ||
| catalogQuery.isLoading || | ||
| providerConfigsQuery.isLoading, | ||
| modelCatalog: catalogQuery.data, | ||
| hasConfiguredModels: hasConfiguredModelsInCatalog(catalogQuery.data), | ||
| }; | ||
| }; |
There was a problem hiding this comment.
I'm not entirely convinced on the value of this being a hook. I know it is re-used in multiple places but I dunno
At each current call site, most of the data is already fetched:
AgentCreatePage: already has chatModels and chatModelConfigs
AgentChatPage: already has chatModels and chatModelConfigs
AgentSettingsUserAgentsPage: already has chatModels and chatModelConfigs
parent AgentsPage now fetches all three pieces, including userChatProviderConfigs
There was a problem hiding this comment.
@DanielleMaywood fair. My thinking was that we fix the root cause rather than add guards in every place that needs to ensure all the data is present.
b45eb9a to
d895b9f
Compare
Follow-up to removing chat_model_configs.provider: dead paths the column removal orphaned or a legacy shim it missed, one pre-existing label inconsistency in the touched surface, and a test-quality fix. Frontend: - Delete getNormalizedModelRef: the getModelOptionsFromConfigs rewrite removed its last production caller; only its own test kept it alive. - Drop the unreachable provider:model legacy branch in resolveModelOptionId (both inputs are config UUIDs), matching the shims already removed from modelDisplayName and chatHelpers. - Remove the dead inner optional chain on ai_provider_id in ModelForm now that the field is required. - Default ModelSelector's formatProviderLabel to the canonical formatter and delete the naive capitalize-first fallback, so every consumer renders "OpenAI" rather than "Openai". - Simplify the now-unused RuntimeModelRef/CatalogModelLike shapes. - Replace the useModelOptions hook with a pure resolveModelSelector function: it uses nothing that requires a hook, so the pages pass their already-fetched queries in and the util centralizes the loading coordination without a second query observer. Backend: - Call prepareManualTitleDebugRun as a bare statement in its test rather than consuming its returns with needless assertions to satisfy dogsled.
d895b9f to
aaa0315
Compare
For release managers
Breaking change (experimental Coder Agents API). The experimental Coder Agents model-configuration API no longer returns
provider; clients get the provider fromai_provider_idinstead. Experimental APIs carry no stability guarantee, so there is no deprecation period.Upgrade with coderd scaled to zero. This release runs a database migration that older coderd versions cannot read. In a rolling upgrade, a replica still on the old version can interrupt an in-progress Coder Agents conversation once the migration runs. Scale coderd to zero before upgrading to avoid interrupting active use. Rollback is unsupported; snapshot the database first.
Changelog entry:
providerfield withai_provider_id(refactor!: drop chat_model_configs provider column #26877)Removes the denormalized
providerstring column fromchat_model_configs. Provider identity now has a single source of truth: theai_providersrow reached throughchat_model_configs.ai_provider_id. Every surface that needs a provider type derives it fromai_providers.typeinstead of a stored copy that a startup backfill had to keep in sync.The stored string was already redundant. The
chat_model_configs_ai_provider_required_when_activeCHECK makesai_provider_idmandatory for non-deleted rows, and routing already preferredai_providers.type, reading the stored string only in an!AIProviderID.Validbranch the constraint makes unreachable for active rows. Those branches now returnsql.ErrNoRows.What changed
ai_providersrow.ORDER BY provideris retargeted toap.type.provider, now sourced fromai_providers.typevia a JOIN, so the BigQuerychat_model_configs.providercolumn and the Nexus dashboards that read it keep working unchanged.ChatModelConfigdropsproviderand promotesai_provider_idto a required field. Consumers resolve provider type fromai_provider_idplusGET /api/experimental/chats/providers. The experimental API carries no stability guarantee, which is what makes this acceptable.000534) drops the column and its two indexes. The obsoleteBackfillChatModelConfigProviderStringsstartup backfill and the unusedDeleteChatModelConfigsByProviderquery are deleted;BackfillBedrockProviderTypestays.ai_provider_idthrough aproviderTypeByIDmap and drops the deadprovider:modellegacy reference shims.Upgrade contract
This ships in a single release with no compatibility window. Production reads the column through
SELECT *positional scans, so a pre-drop binary fails config reads the instant the column disappears. Operators must scale to zero before upgrading. Coder does not support rollbacks.Notes for reviewers
chatProviderConfigs()as the lookup source everywhere, but that endpoint (GET /api/v2/ai/providers) is owner-only, while the model selector serves all users. Using it everywhere would empty the selector for non-owners. Owner-only admin pages keepchatProviderConfigs(); user-facing chat pages use the all-usersuserChatProviderConfigs(). This preserves the selector for non-owners.providerbecause the analytics pipeline has noai_provider_id/typedimension and both ends are owned internally; the API has a documented alternative.Tool > MCP Tool Completed,AgentChatPageView > Scroll To Bottom Button Works With Inverse Scroll) fail in the local sandbox. Verified they fail identically on unmodifiedmainand share no code with this change; they are an environment limitation, left for CI to validate.Implementation plan (source of truth for this change)
Remove
providerfromchat_model_configs(CODAGT-599)Direction (approval surface)
Outcome
The denormalized
providerstring is gone from Agents storage. Provideridentity for a model config has a single source of truth: the
ai_providersrow reached through
chat_model_configs.ai_provider_id. Every surface thatstill needs a provider type derives it from
ai_providers.typeinstead ofreading a stored copy.
Observable end state
chat_model_configshas noprovidercolumn, and noidx_chat_model_configs_provideror
idx_chat_model_configs_provider_modelindex.ChatModelConfigresponse has noproviderfield.Consumers resolve provider type from
ai_provider_idplusGET /api/experimental/chats/providers(id->providertype).references using the provider type derived from
ai_provider_id, not a storedproviderstring.providerfor each model config, now computed fromai_providers.typeat snapshot time. The BigQuerychat_model_configs.providercolumn and the Nexus dashboards that read it keep working unchanged.
BackfillChatModelConfigProviderStringsand the unusedDeleteChatModelConfigsByProviderquery are deleted.BackfillBedrockProviderTyperemains.
ai_provider_ida required (non-omitempty) fieldfor the configs it returns, which are always active, so consumers rely on it
as the sole provider identity.
providerhave a notice commentdescribing the removal and the
ai_provider_idreplacement path.Recommended direction
Remove the column and every dependence on the stored string in a single
release, treating the change as one coordinated unit across schema, backend,
SDK, and frontend. Keep
provideralive only where a consumer has a verifieddependency and no
ai_provider_id/typedimension to derive from. Thatcondition holds for exactly one surface: the telemetry analytics projection.
Reason
Provider type already lives authoritatively in
ai_providers.type, reachableon every active row through
ai_provider_id, which the CHECK constraintchat_model_configs_ai_provider_required_when_activemakes mandatory fornon-deleted rows (
coderd/database/dump.sql:1927). Routing already prefersai_providers.typeand reads the stored string only in an!AIProviderID.Validbranch that the constraint makes unreachable for activerows (
coderd/x/chatd/model_routing_direct.go:85-92,coderd/x/chatd/subagent.go:536-553).The stored
provideris therefore a duplicate the system maintains with astartup backfill but no longer needs as a source of truth.
The telemetry projection is kept because it is the only provider-identity
dimension in the analytics pipeline: the BigQuery schema
bqChatModelConfig(
coder/coder-telemetry-server,convert.go) carriesproviderbut noai_provider_idortype, and Nexus queries readcoder-telemetry.coder.chat_model_configs.providerdirectly (
coder/nexus,util/sql/coder_agents_provider_trends.sqland foursibling queries). Deriving that field from
ai_providers.typepreserves theanalytics contract while removing the stored duplication, so the projection is
computed from the source of truth rather than a denormalized copy.
High-level shape
ai_providersrow at each read site, retargetORDER BY provider, andsource the telemetry
providerfrom a JOIN.codersdkand the TypeScripttypes.
ai_provider_id, including fixtures and stories.code.
Ruled-out directions
Ruled out by operator decision to ship in one release on the assumption of
scale-to-zero cutovers. Recorded as a risk below, since production reads the
column through
SELECT *positional scans, so a running pre-drop binaryfails those scans the instant the column disappears.
satisfy the issue goal and leaves a dead column plus an obsolete backfill in
place.
providerin the HTTP/SDK response. Ruled out by operatordecision.
coder-mobile-androidis not an officially supported project, soits dependence does not justify retaining the API field; it gets a notice
comment instead.
providerfrom telemetry too and migrate the analytics pipeline.Ruled out by operator decision (kept derived). It would require coordinated
changes in
coder-telemetry-server(BigQuery schema) andnexus(five SQLqueries) plus a story for historical BigQuery rows that only have
provider.Decisions already made
to zero before upgrading. This is the accepted upgrade contract for the
change.
ai_provider_idbecomes a required (non-omitempty) field in the API/SDK andTypeScript response, since those endpoints return only active configs and
active configs always have it.
providerfield is removed end-to-end; consumers migrate toai_provider_id+ the providers endpoint.ai_provider_idend-to-end.provider, sourced fromai_providers.type.or a coordinated release.
Assumptions
chat_model_configsrow has a non-nullai_provider_id. Verified at the database level: the CHECK constraintchat_model_configs_ai_provider_required_when_activewas added withoutNOT VALID(migration000505), so Postgres validated existing rows on addand the migration could not have succeeded with a non-conforming active row.
The single production insert path also requires it (
coderd/exp_chats.go:6920-6923),the update path never nulls it (
coderd/exp_chats.go:7140-7162), and the FKhas no
ON DELETE SET NULLwhileai_providerssoft-deletes(
coderd/database/dump.sql:5004). Soft-deleted rows may have nullai_provider_idbut are never returned by the model-config endpoints.provider:modelstring. Verified:chats.last_model_config_idis auuidcolumn (coderd/database/models.go:4786,@last_model_config_id::uuid; TStypesGenerated.ts:1563) resolved as a UUID(
coderd/x/chatd/chatd.go:4425-4442), andModelSelectorOption.idis alwaysthe config UUID. The frontend
provider:modelmatching is dead and is removedin Part 4, not preserved.
ai_provider_id. Verified:GET /api/experimental/chats/providersreturnsChatProviderConfig{ID, Provider}where
Provideris the type (codersdk/chats.go:1167-1180).Constraints
startup-backfill approach used by PR fix: backfill legacy Bedrock AI provider rows and stale model config strings #26155 was specific to reading
dbcrypt-encrypted settings and does not apply here.
(
coderd/coderd.go:1192), which is what makes the field removal acceptable.Tradeoffs
derived
provider, the API does not. This is intentional. Telemetry has adownstream consumer with no alternative dimension and both ends are owned
internally; the API has a documented alternative (
ai_provider_id+ providersendpoint).
Risks affecting the direction
providerviaSELECT *, so a pre-drop replica still serving during a rolling upgrade wouldfail config reads once the column is gone. Accepted: the change provides no
compatibility guarantees during migration and requires operators to scale to
zero before upgrading. Coder does not support rollbacks
(
docs/install/upgrade.md). This is a documented operating contract, not anopen mitigation.
ai_provider_id. Today the TypeScript type marks itoptional and fixtures omit it. Making the response field required removes the
runtime case for active configs (backend invariants guarantee the FK), so the
residual work is mechanical: update fixtures and stories that omit it and
remove the now-dead optional-handling branches rather than leaving them to
silently fall back to a removed field.
Deferred decisions
ai_provider_idto strictlyNOT NULL(dropping thedeleted = TRUE OR ...exemption). Out of scope here; soft-deleted rowslegitimately hold null.
providerstring onto a provider-type dimension. Deferred; not required by this change.
Implementation detail (preserves the approved direction)
Line numbers are anchors as of the researched revision and will drift; the named
symbols (functions, queries, struct fields) are authoritative. Grep the symbol
if a line number no longer matches.
Part 1: backend stops reading the column
Replace each read of the stored
Providerwith the provider type from theloaded
ai_providersrow.coderd/x/chatd/model_routing_direct.go:84-92andcoderd/x/chatd/subagent.go:172-190,553. In the!AIProviderID.Validbranch,return
sql.ErrNoRows(the signal these call sites already treat as "nousable model config") instead of reading the stored string. Active rows always
have the FK, so the branch is unreachable for them.
coderd/x/chatd/generation_preparer.go:263(chatFileResolver)takes the resolved provider type from the route loaded for the call,
string(route.Provider.Type).coderd/x/chatd/chatd.go, two treatments:chat_debug_runs.provider, aseparate column kept by this change. Set it from
string(route.Provider.Type)at the
WrapModelRecorderOptions.Providersite (:2622) and theCreateRunProvidersite (:2653), whererouteis resolved in scope.slog.F("provider", ...)(:2608,2615,2665,2791,2806) sitin failure branches where no route resolved: delete the field.
coderd/exp_chats.go:889-917buildsConfiguredModel.Provider;source it from
ai_providers.type. This requiresGetEnabledChatModelConfigsto return
ap.type(it already JOINsai_providers,coderd/database/queries/chatmodelconfigs.sql:37-38).coderd/exp_chats.go:6533fromGetChatCostPerModel(
coderd/database/queries/chats.sql). This query joinschat_model_configswithout a
deleted = FALSEfilter, so it can reference soft-deleted configsthat may carry a null
ai_provider_id. UseLEFT JOIN ai_providers ap ON ap.id = cmc.ai_provider_id(noap.deletedfilter, so soft-deleted providersstill resolve) and select
COALESCE(ap.type::text, '') AS provider. Dropcmc.providerfrom the SELECT and theGROUP BY.ORDER BY providerclauses(
coderd/database/queries/chatmodelconfigs.sql:27,45) toap.type(andmodel).Part 2: telemetry keeps
provider, derivedGetChatModelConfigsForTelemetry(coderd/database/queries/chats.sql,currently
SELECT id, provider, model, ... WHERE deleted = false) toJOIN ai_providers ap ON ap.id = cmc.ai_provider_idand selectap.type AS provider. The query already filtersdeleted = false, so everyrow has a non-null
ai_provider_id; a plain INNER JOIN is correct and nocoalesce is needed.
coderd/telemetry/telemetry.go:2304and the struct field:2475keep theirshape.
bqChatModelConfigand Nexus SQL need no change.Part 3: SDK drops the field
Providerfield fromcodersdk.ChatModelConfig(
codersdk/chats.go:1248-1262) and fromconvertChatModelConfig(
coderd/exp_chats.go:7505).Providerfield fromCreateChatModelConfigRequestandUpdateChatModelConfigRequest(both define it,codersdk/chats.go) and theprovider-string handling in the handlers: delete the legacy branch at
coderd/exp_chats.go:7128-7137(the "AI provider ID is required when updatingprovider" path) and the insert/update param wiring at
coderd/exp_chats.go:6997,7220,7429. Leave the requestAIProviderIDfieldsas optional pointers: create validates presence at runtime
(
coderd/exp_chats.go:6920-6923), update omits it to keep the existing value.Only the response field changes shape.
ai_provider_idrequired: changecodersdk.ChatModelConfig.AIProviderIDfrom
*uuid.UUIDwithomitempty(codersdk/chats.go:1251) to a non-pointeruuid.UUID. The list and get endpoints return onlydeleted = FALSEconfigs,which always carry the FK, so the field is never absent in responses.
convertChatModelConfig(coderd/exp_chats.go:7498) is the only constructionsite; it assigns
config.AIProviderID.UUIDdirectly and relies on the CHECKconstraint, with no runtime null-handling. The invariant is covered by a test,
not by a defensive branch.
make gento regeneratecodersdkconsumers and the TypeScript types:providerleavesChatModelConfig,CreateChatModelConfigRequest,UpdateChatModelConfigRequest, andai_provider_idbecomes required (ai_provider_id: string)(
site/src/api/typesGenerated.ts:2488,2489,3502,8890).Part 4: frontend moves onto
ai_provider_idIntroduce one provider-type lookup and use it everywhere a provider type is
needed; do not read
config.provideranywhere. BuildproviderTypeByID: ReadonlyMap<string, string>fromchatProviderConfigs()(
site/src/api/queries/chats.ts:1847-1853), mappingChatProviderConfig.idtoChatProviderConfig.provider. Each site below resolves the provider type asproviderTypeByID.get(config.ai_provider_id)and receives the map as a prop orargument from the page that already loads the providers query.
ModelRow.tsx:39andUserCompactionThresholdSettings.tsx:285.UserCompactionThresholdSettingsgains aproviderTypeByIDprop sourced byits parent page from
chatProviderConfigs(). Replaceprovider={modelConfig.provider}withprovider={providerTypeByID.get(modelConfig.ai_provider_id)}.providerTypeByIDparameter togetModelOptionsFromConfigs(modelOptions.ts:73,185,197,205) and setModelSelectorOption.providerfrom the map; callers pass the map. Update thegrouping and labels in
ModelSelector.tsx:83,88,127,129and the option builtin
SubagentModelOverrideSettings.tsx:48to use the same value.ModelsPage.tsx:24sorts by the mapped provider type.provider:modelmatching inmodelDisplayName.ts:29,42,48andchatHelpers.ts:84,101, and the.providerreads that feed it. It cannot match real data:
getModelOptionsFromConfigsalways sets
ModelSelectorOption.idto the config UUID, andChat.last_model_config_idis a uuid string (typesGenerated.ts:1563).provider-string fallback branches inproviderStates.ts:124-133,224-236andAgentSettingsAPIKeysPageView.tsx:91-95;they are dead once
ai_provider_idis required.providerinModelForm.tsx:155,184; send onlyai_provider_id.provideror omitsai_provider_idon a
ChatModelConfig:testHelpers/chatModels.tsandModelsPage/testFixtures.ts, plus the stories the TypeScript compiler flagsonce
ai_provider_idis required (the required field turns every omissioninto a compile error, so the compiler enumerates them). For each, add
ai_provider_idand removeprovider.Part 5: schema migration
DROP INDEX idx_chat_model_configs_provider,idx_chat_model_configs_provider_model; thenALTER TABLE chat_model_configs DROP COLUMN provider..down.sqlmust restore the original schema exactly, since themigration test runs up then down:
ADD COLUMN provider text; backfillUPDATE chat_model_configs cmc SET provider = ap.type FROM ai_providers ap WHERE ap.id = cmc.ai_provider_id; setprovider = ''for any rows still nullafter the backfill (soft-deleted, unlinked);
ALTER COLUMN provider SET NOT NULL; recreate both indexes.coderd/database/dump.sqland the migration test fixtures viamake gen.Part 6: remove obsolete code
BackfillChatModelConfigProviderStrings(coderd/ai_providers_backfill.go:74-95)and its startup call (
cli/server.go:1119-1122). KeepBackfillBedrockProviderType.BackfillChatModelConfigProviderquery(
coderd/database/queries/chatmodelconfigs.sql:147-166) and the unusedDeleteChatModelConfigsByProvider(:136-145);DeleteChatModelConfigsByAIProviderID(
:168) covers provider-scoped deletion.Providerfromdatabase.ChatModelConfigand regeneratequeries.sql.go,querier.go,dbauthz,dbmetrics,dbmock. Remove thedead-query plumbing and its
dbauthz_test.gocase. No audit-table entryexists for
chat_model_configs.Part 7: external consumers (coder org)
describing the removal and the replacement path (
ai_provider_idplusGET /api/experimental/chats/providers, mappingid->providertype):coder/coder-mobile-android(ChatModelConfigDto.provider),coder/demo-aigov-rhaiis-rhsummit-2026,coder/usgov-coderdemo,coder/pixel-playground.Ordering and dependencies
Parts 1, 2, and 3 must land before Part 5; the column cannot drop while code
still scans it. Part 4 depends on Part 3 (regenerated types). Part 6 deletes
code only after Part 1 removes the last reader. Part 7 is independent and can
proceed in parallel.
Verification
coderd/database/dump.sqlfor any view ortrigger referencing
chat_model_configs.provider; precedent column drops hadto rebuild dependent views. None is expected (the relevant SQL are queries,
not views), but confirm.
make gen,make lint,make testfor backend and generated code.pages exercise icon, grouping, sort, and legacy-reference behavior without the
providerfield.providervalue after theJOIN change; the query returns only active rows. Confirm the cost breakdown
still returns a row for messages whose model config was later soft-deleted.
Invariants
chat_model_configsrows always have a resolvableai_provider_id;provider type is read from
ai_providers.type, never from a stored copy.providervalue equals the linked provider'stype. The costbreakdown tolerates soft-deleted configs with a null
ai_provider_idbyemitting an empty provider rather than dropping the row.
providercolumn fromchat_model_configsafter themigration.
ChatModelConfigthe API returns carries a non-nullai_provider_id,because those endpoints return only active configs. This holds only while no
endpoint returns soft-deleted configs and the
chat_model_configs_ai_provider_required_when_activeCHECK stays in place;both are load-bearing for the required-field contract.
References
coderd/database/dump.sql:1910-1930,4628,4630;migrations
000503-000505(PRs feat: add AI provider schema expansion #25412, feat: remove legacy chat provider tables #25416).coderd/ai_providers_backfill.go;cli/server.go:1119-1122(PR fix: backfill legacy Bedrock AI provider rows and stale model config strings #26155 added the startup backfills only).
coderd/telemetry/telemetry.go:2304,2475;coder/coder-telemetry-serverconvert.go(bqChatModelConfig);coder/nexusutil/sql/coder_agents_provider_trends.sqland siblings.coder/coder-mobile-androidAgentDtos.kt(
ChatModelConfigDto);coder/demo-aigov-rhaiis-rhsummit-2026services/bridge/internal/coder/coder.go.