From f8ef81d526171e2c928baf16193f6360c9f7461d Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:52:28 +0000 Subject: [PATCH 1/4] refactor(coderd): stop storing chat gateway key IDs and drop the columns --- coderd/database/dbgen/dbgen.go | 10 - coderd/database/dump.sql | 2 - ...547_drop_chat_gateway_key_columns.down.sql | 5 + ...00547_drop_chat_gateway_key_columns.up.sql | 5 + coderd/database/models.go | 2 - coderd/database/querier_test.go | 6 - coderd/database/queries.sql.go | 85 ++---- coderd/database/queries/chats.sql | 8 +- coderd/exp_chats.go | 2 +- coderd/exp_chats_test.go | 59 ++-- coderd/x/chatd/ARCHITECTURE.md | 2 +- coderd/x/chatd/chatd.go | 99 +------ coderd/x/chatd/chatd_internal_test.go | 14 +- coderd/x/chatd/chatd_test.go | 260 ++---------------- coderd/x/chatd/chatstate/machine_test.go | 10 - coderd/x/chatd/chatstate/messages.go | 5 - .../chatstate/synthetic_cancellation_test.go | 1 - coderd/x/chatd/chatstate/transitions.go | 33 +-- .../chatstate/transitions_helpers_test.go | 9 +- .../chatstate/transitions_matrix_test.go | 1 - coderd/x/chatd/chatstate_bridge.go | 5 +- coderd/x/chatd/generation.go | 1 - .../generation_preparer_internal_test.go | 75 ++++- coderd/x/chatd/helpers_test.go | 1 - coderd/x/chatd/message_conversion.go | 2 - coderd/x/chatd/model_routing_internal_test.go | 4 - coderd/x/chatd/stream_loop.go | 3 +- coderd/x/chatd/subagent.go | 7 +- coderd/x/chatd/subagent_internal_test.go | 83 ------ coderd/x/chatd/synthetickey_internal_test.go | 16 +- coderd/x/chatd/tasks_test.go | 1 - coderd/x/chatd/turn_summary_internal_test.go | 2 - 32 files changed, 202 insertions(+), 616 deletions(-) create mode 100644 coderd/database/migrations/000547_drop_chat_gateway_key_columns.down.sql create mode 100644 coderd/database/migrations/000547_drop_chat_gateway_key_columns.up.sql diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 0404f2ec3788b..dd40c0e38eea4 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -122,20 +122,10 @@ func ChatMessage(t testing.TB, db database.Store, seed database.ChatMessage) dat content = string(seed.Content.RawMessage) } role := takeFirst(seed.Role, database.ChatMessageRoleUser) - apiKeyID := seed.APIKeyID.String - // Mint a real API key for user turns so the api_key_id foreign key is - // satisfied. Without a creator we leave it empty, which the insert query - // stores as NULL. - if role == database.ChatMessageRoleUser && apiKeyID == "" && - seed.CreatedBy.Valid && seed.CreatedBy.UUID != uuid.Nil { - key, _ := APIKey(t, db, database.APIKey{UserID: seed.CreatedBy.UUID}) - apiKeyID = key.ID - } msgs, err := db.InsertChatMessages(genCtx, database.InsertChatMessagesParams{ ChatID: seed.ChatID, CreatedBy: []uuid.UUID{seed.CreatedBy.UUID}, - APIKeyID: []string{apiKeyID}, ModelConfigID: []uuid.UUID{seed.ModelConfigID.UUID}, ReasoningEffort: []string{string(seed.ReasoningEffort.ChatReasoningEffort)}, Role: []database.ChatMessageRole{role}, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 2313ebf1df887..a3c458a7cdbb9 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1963,7 +1963,6 @@ CREATE TABLE chat_messages ( runtime_ms bigint, deleted boolean DEFAULT false NOT NULL, provider_response_id text, - api_key_id text, revision bigint NOT NULL, reasoning_effort chat_reasoning_effort, search_tsv tsvector @@ -2016,7 +2015,6 @@ CREATE TABLE chat_queued_messages ( content jsonb NOT NULL, created_at timestamp with time zone DEFAULT now() NOT NULL, model_config_id uuid, - api_key_id text, "position" bigint DEFAULT nextval('chat_queued_messages_position_seq'::regclass) NOT NULL, created_by uuid NOT NULL, reasoning_effort chat_reasoning_effort diff --git a/coderd/database/migrations/000547_drop_chat_gateway_key_columns.down.sql b/coderd/database/migrations/000547_drop_chat_gateway_key_columns.down.sql new file mode 100644 index 0000000000000..90afeb1d11c6a --- /dev/null +++ b/coderd/database/migrations/000547_drop_chat_gateway_key_columns.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE chat_messages + ADD COLUMN api_key_id text; + +ALTER TABLE chat_queued_messages + ADD COLUMN api_key_id text; diff --git a/coderd/database/migrations/000547_drop_chat_gateway_key_columns.up.sql b/coderd/database/migrations/000547_drop_chat_gateway_key_columns.up.sql new file mode 100644 index 0000000000000..d72c336cba484 --- /dev/null +++ b/coderd/database/migrations/000547_drop_chat_gateway_key_columns.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE chat_messages + DROP COLUMN api_key_id; + +ALTER TABLE chat_queued_messages + DROP COLUMN api_key_id; diff --git a/coderd/database/models.go b/coderd/database/models.go index 73c11d14680b2..43393f965565b 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -5119,7 +5119,6 @@ type ChatMessage struct { RuntimeMs sql.NullInt64 `db:"runtime_ms" json:"runtime_ms"` Deleted bool `db:"deleted" json:"deleted"` ProviderResponseID sql.NullString `db:"provider_response_id" json:"provider_response_id"` - APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"` Revision int64 `db:"revision" json:"revision"` // Stores the selected effort for the turn triggered by this message. ReasoningEffort NullChatReasoningEffort `db:"reasoning_effort" json:"reasoning_effort"` @@ -5151,7 +5150,6 @@ type ChatQueuedMessage struct { Content json.RawMessage `db:"content" json:"content"` CreatedAt time.Time `db:"created_at" json:"created_at"` ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"` - APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"` Position int64 `db:"position" json:"position"` CreatedBy uuid.UUID `db:"created_by" json:"created_by"` // Stores the selected effort until the queued row is promoted. diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index c590dc0640723..596677f8b5397 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -11939,12 +11939,9 @@ func TestInsertChatMessages(t *testing.T) { insertMessage := func(t *testing.T, store database.Store, ctx context.Context, chatID, userID, modelConfigID uuid.UUID, content string) { t.Helper() - apiKey, _ := dbgen.APIKey(t, store, database.APIKey{ID: uuid.NewString(), UserID: userID}) - _, err := store.InsertChatMessages(ctx, database.InsertChatMessagesParams{ ChatID: chatID, CreatedBy: []uuid.UUID{userID}, - APIKeyID: []string{apiKey.ID}, ModelConfigID: []uuid.UUID{modelConfigID}, Role: []database.ChatMessageRole{database.ChatMessageRoleUser}, ContentVersion: []int16{chatprompt.CurrentContentVersion}, @@ -12003,12 +12000,9 @@ func TestInsertChatMessages(t *testing.T) { t.Parallel() store, ctx, user, chat, _, modelConfigA := setupChat(t) - apiKey, _ := dbgen.APIKey(t, store, database.APIKey{ID: uuid.NewString(), UserID: user.ID}) - msgs, err := store.InsertChatMessages(ctx, database.InsertChatMessagesParams{ ChatID: chat.ID, CreatedBy: []uuid.UUID{user.ID, uuid.Nil, uuid.Nil}, - APIKeyID: []string{apiKey.ID, "", ""}, ModelConfigID: []uuid.UUID{modelConfigA.ID, modelConfigA.ID, modelConfigA.ID}, Role: []database.ChatMessageRole{database.ChatMessageRoleUser, database.ChatMessageRoleAssistant, database.ChatMessageRoleTool}, ContentVersion: []int16{chatprompt.CurrentContentVersion, chatprompt.CurrentContentVersion, chatprompt.CurrentContentVersion}, diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index d57b081261582..ecd0d926930ab 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -7488,7 +7488,7 @@ func (q *sqlQuerier) GetChatHeartbeat(ctx context.Context, arg GetChatHeartbeatP const getChatMessageByID = `-- name: GetChatMessageByID :one SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision, reasoning_effort, search_tsv + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, revision, reasoning_effort, search_tsv FROM chat_messages WHERE @@ -7521,7 +7521,6 @@ func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMess &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, - &i.APIKeyID, &i.Revision, &i.ReasoningEffort, &i.SearchTsv, @@ -7614,7 +7613,7 @@ func (q *sqlQuerier) GetChatMessageSummariesPerChat(ctx context.Context, created const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision, reasoning_effort, search_tsv + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, revision, reasoning_effort, search_tsv FROM chat_messages WHERE @@ -7662,7 +7661,6 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, - &i.APIKeyID, &i.Revision, &i.ReasoningEffort, &i.SearchTsv, @@ -7682,7 +7680,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes const getChatMessagesByChatIDAscPaginated = `-- name: GetChatMessagesByChatIDAscPaginated :many SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision, reasoning_effort, search_tsv + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, revision, reasoning_effort, search_tsv FROM chat_messages WHERE @@ -7733,7 +7731,6 @@ func (q *sqlQuerier) GetChatMessagesByChatIDAscPaginated(ctx context.Context, ar &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, - &i.APIKeyID, &i.Revision, &i.ReasoningEffort, &i.SearchTsv, @@ -7753,7 +7750,7 @@ func (q *sqlQuerier) GetChatMessagesByChatIDAscPaginated(ctx context.Context, ar const getChatMessagesByChatIDDescPaginated = `-- name: GetChatMessagesByChatIDDescPaginated :many SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision, reasoning_effort, search_tsv + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, revision, reasoning_effort, search_tsv FROM chat_messages WHERE @@ -7817,7 +7814,6 @@ func (q *sqlQuerier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, a &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, - &i.APIKeyID, &i.Revision, &i.ReasoningEffort, &i.SearchTsv, @@ -7837,7 +7833,7 @@ func (q *sqlQuerier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, a const getChatMessagesByRevisionForStream = `-- name: GetChatMessagesByRevisionForStream :many SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision, reasoning_effort, search_tsv + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, revision, reasoning_effort, search_tsv FROM chat_messages WHERE @@ -7884,7 +7880,6 @@ func (q *sqlQuerier) GetChatMessagesByRevisionForStream(ctx context.Context, arg &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, - &i.APIKeyID, &i.Revision, &i.ReasoningEffort, &i.SearchTsv, @@ -7920,7 +7915,7 @@ WITH latest_compressed_summary AS ( 1 ) SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision, reasoning_effort, search_tsv + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, revision, reasoning_effort, search_tsv FROM chat_messages WHERE @@ -7992,7 +7987,6 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, - &i.APIKeyID, &i.Revision, &i.ReasoningEffort, &i.SearchTsv, @@ -8059,7 +8053,7 @@ func (q *sqlQuerier) GetChatModelConfigsForTelemetry(ctx context.Context) ([]Get } const getChatQueuedMessageByID = `-- name: GetChatQueuedMessageByID :one -SELECT id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by, reasoning_effort FROM chat_queued_messages +SELECT id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort FROM chat_queued_messages WHERE id = $1::bigint AND chat_id = $2::uuid ` @@ -8077,7 +8071,6 @@ func (q *sqlQuerier) GetChatQueuedMessageByID(ctx context.Context, arg GetChatQu &i.Content, &i.CreatedAt, &i.ModelConfigID, - &i.APIKeyID, &i.Position, &i.CreatedBy, &i.ReasoningEffort, @@ -8086,7 +8079,7 @@ func (q *sqlQuerier) GetChatQueuedMessageByID(ctx context.Context, arg GetChatQu } const getChatQueuedMessageHead = `-- name: GetChatQueuedMessageHead :one -SELECT id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by, reasoning_effort FROM chat_queued_messages +SELECT id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort FROM chat_queued_messages WHERE chat_id = $1::uuid ORDER BY position ASC, id ASC LIMIT 1 @@ -8102,7 +8095,6 @@ func (q *sqlQuerier) GetChatQueuedMessageHead(ctx context.Context, chatID uuid.U &i.Content, &i.CreatedAt, &i.ModelConfigID, - &i.APIKeyID, &i.Position, &i.CreatedBy, &i.ReasoningEffort, @@ -8111,7 +8103,7 @@ func (q *sqlQuerier) GetChatQueuedMessageHead(ctx context.Context, chatID uuid.U } const getChatQueuedMessages = `-- name: GetChatQueuedMessages :many -SELECT id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by, reasoning_effort FROM chat_queued_messages +SELECT id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort FROM chat_queued_messages WHERE chat_id = $1 ORDER BY created_at ASC, id ASC ` @@ -8131,7 +8123,6 @@ func (q *sqlQuerier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID &i.Content, &i.CreatedAt, &i.ModelConfigID, - &i.APIKeyID, &i.Position, &i.CreatedBy, &i.ReasoningEffort, @@ -8150,7 +8141,7 @@ func (q *sqlQuerier) GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID } const getChatQueuedMessagesByPosition = `-- name: GetChatQueuedMessagesByPosition :many -SELECT id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by, reasoning_effort FROM chat_queued_messages +SELECT id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort FROM chat_queued_messages WHERE chat_id = $1::uuid ORDER BY position ASC, id ASC ` @@ -8171,7 +8162,6 @@ func (q *sqlQuerier) GetChatQueuedMessagesByPosition(ctx context.Context, chatID &i.Content, &i.CreatedAt, &i.ModelConfigID, - &i.APIKeyID, &i.Position, &i.CreatedBy, &i.ReasoningEffort, @@ -9283,7 +9273,7 @@ func (q *sqlQuerier) GetDatabaseNow(ctx context.Context) (time.Time, error) { const getLastChatMessageByRole = `-- name: GetLastChatMessageByRole :one SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision, reasoning_effort, search_tsv + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, revision, reasoning_effort, search_tsv FROM chat_messages WHERE @@ -9326,7 +9316,6 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, - &i.APIKeyID, &i.Revision, &i.ReasoningEffort, &i.SearchTsv, @@ -9791,7 +9780,7 @@ WITH batch AS ( SELECT ( SELECT val - FROM UNNEST($4::uuid[]) + FROM UNNEST($3::uuid[]) WITH ORDINALITY AS t(val, ord) WHERE val != '00000000-0000-0000-0000-000000000000'::uuid ORDER BY ord DESC @@ -9799,7 +9788,7 @@ WITH batch AS ( ) AS last_model_config_id, ( SELECT NULLIF(val, '')::chat_reasoning_effort - FROM UNNEST($5::text[]) + FROM UNNEST($4::text[]) WITH ORDINALITY AS t(val, ord) WHERE val != '' ORDER BY ord DESC @@ -9823,7 +9812,6 @@ updated_chat AS ( INSERT INTO chat_messages ( chat_id, created_by, - api_key_id, model_config_id, reasoning_effort, role, @@ -9844,31 +9832,29 @@ INSERT INTO chat_messages ( SELECT $1::uuid, NULLIF(UNNEST($2::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid), - NULLIF(UNNEST($3::text[]), ''), - NULLIF(UNNEST($4::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid), - NULLIF(UNNEST($5::text[]), '')::chat_reasoning_effort, - UNNEST($6::chat_message_role[]), - UNNEST($7::text[])::jsonb, - UNNEST($8::smallint[]), - UNNEST($9::chat_message_visibility[]), + NULLIF(UNNEST($3::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid), + NULLIF(UNNEST($4::text[]), '')::chat_reasoning_effort, + UNNEST($5::chat_message_role[]), + UNNEST($6::text[])::jsonb, + UNNEST($7::smallint[]), + UNNEST($8::chat_message_visibility[]), + NULLIF(UNNEST($9::bigint[]), 0), NULLIF(UNNEST($10::bigint[]), 0), NULLIF(UNNEST($11::bigint[]), 0), NULLIF(UNNEST($12::bigint[]), 0), NULLIF(UNNEST($13::bigint[]), 0), NULLIF(UNNEST($14::bigint[]), 0), NULLIF(UNNEST($15::bigint[]), 0), - NULLIF(UNNEST($16::bigint[]), 0), - UNNEST($17::boolean[]), - NULLIF(UNNEST($18::bigint[]), 0), - NULLIF(UNNEST($19::bigint[]), 0) + UNNEST($16::boolean[]), + NULLIF(UNNEST($17::bigint[]), 0), + NULLIF(UNNEST($18::bigint[]), 0) RETURNING - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, api_key_id, revision, reasoning_effort, search_tsv + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by, content_version, total_cost_micros, runtime_ms, deleted, provider_response_id, revision, reasoning_effort, search_tsv ` type InsertChatMessagesParams struct { ChatID uuid.UUID `db:"chat_id" json:"chat_id"` CreatedBy []uuid.UUID `db:"created_by" json:"created_by"` - APIKeyID []string `db:"api_key_id" json:"api_key_id"` ModelConfigID []uuid.UUID `db:"model_config_id" json:"model_config_id"` ReasoningEffort []string `db:"reasoning_effort" json:"reasoning_effort"` Role []ChatMessageRole `db:"role" json:"role"` @@ -9891,7 +9877,6 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa rows, err := q.db.QueryContext(ctx, insertChatMessages, arg.ChatID, pq.Array(arg.CreatedBy), - pq.Array(arg.APIKeyID), pq.Array(arg.ModelConfigID), pq.Array(arg.ReasoningEffort), pq.Array(arg.Role), @@ -9938,7 +9923,6 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa &i.RuntimeMs, &i.Deleted, &i.ProviderResponseID, - &i.APIKeyID, &i.Revision, &i.ReasoningEffort, &i.SearchTsv, @@ -9957,17 +9941,16 @@ func (q *sqlQuerier) InsertChatMessages(ctx context.Context, arg InsertChatMessa } const insertChatQueuedMessage = `-- name: InsertChatQueuedMessage :one -INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, api_key_id, created_by) +INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, created_by) SELECT $1::uuid, $2::jsonb, $3::uuid, $4::chat_reasoning_effort, - $5::text, chats.owner_id FROM chats WHERE chats.id = $1::uuid -RETURNING id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by, reasoning_effort +RETURNING id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort ` type InsertChatQueuedMessageParams struct { @@ -9975,7 +9958,6 @@ type InsertChatQueuedMessageParams struct { Content json.RawMessage `db:"content" json:"content"` ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"` ReasoningEffort NullChatReasoningEffort `db:"reasoning_effort" json:"reasoning_effort"` - APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"` } // Legacy queue insertion path. When no caller-supplied creator exists, @@ -9987,7 +9969,6 @@ func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChat arg.Content, arg.ModelConfigID, arg.ReasoningEffort, - arg.APIKeyID, ) var i ChatQueuedMessage err := row.Scan( @@ -9996,7 +9977,6 @@ func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChat &i.Content, &i.CreatedAt, &i.ModelConfigID, - &i.APIKeyID, &i.Position, &i.CreatedBy, &i.ReasoningEffort, @@ -10005,16 +9985,15 @@ func (q *sqlQuerier) InsertChatQueuedMessage(ctx context.Context, arg InsertChat } const insertChatQueuedMessageWithCreator = `-- name: InsertChatQueuedMessageWithCreator :one -INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, api_key_id, created_by) +INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, created_by) VALUES ( $1::uuid, $2::jsonb, $3::uuid, $4::chat_reasoning_effort, - $5::text, - $6::uuid + $5::uuid ) -RETURNING id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by, reasoning_effort +RETURNING id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort ` type InsertChatQueuedMessageWithCreatorParams struct { @@ -10022,7 +10001,6 @@ type InsertChatQueuedMessageWithCreatorParams struct { Content json.RawMessage `db:"content" json:"content"` ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"` ReasoningEffort NullChatReasoningEffort `db:"reasoning_effort" json:"reasoning_effort"` - APIKeyID sql.NullString `db:"api_key_id" json:"api_key_id"` CreatedBy uuid.UUID `db:"created_by" json:"created_by"` } @@ -10035,7 +10013,6 @@ func (q *sqlQuerier) InsertChatQueuedMessageWithCreator(ctx context.Context, arg arg.Content, arg.ModelConfigID, arg.ReasoningEffort, - arg.APIKeyID, arg.CreatedBy, ) var i ChatQueuedMessage @@ -10045,7 +10022,6 @@ func (q *sqlQuerier) InsertChatQueuedMessageWithCreator(ctx context.Context, arg &i.Content, &i.CreatedAt, &i.ModelConfigID, - &i.APIKeyID, &i.Position, &i.CreatedBy, &i.ReasoningEffort, @@ -10514,7 +10490,7 @@ WHERE id = ( ORDER BY cqm.created_at ASC, cqm.id ASC LIMIT 1 ) -RETURNING id, chat_id, content, created_at, model_config_id, api_key_id, position, created_by, reasoning_effort +RETURNING id, chat_id, content, created_at, model_config_id, position, created_by, reasoning_effort ` func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error) { @@ -10526,7 +10502,6 @@ func (q *sqlQuerier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) &i.Content, &i.CreatedAt, &i.ModelConfigID, - &i.APIKeyID, &i.Position, &i.CreatedBy, &i.ReasoningEffort, diff --git a/coderd/database/queries/chats.sql b/coderd/database/queries/chats.sql index 8131fe50d6745..62f243f645d71 100644 --- a/coderd/database/queries/chats.sql +++ b/coderd/database/queries/chats.sql @@ -895,7 +895,6 @@ updated_chat AS ( INSERT INTO chat_messages ( chat_id, created_by, - api_key_id, model_config_id, reasoning_effort, role, @@ -916,7 +915,6 @@ INSERT INTO chat_messages ( SELECT @chat_id::uuid, NULLIF(UNNEST(@created_by::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid), - NULLIF(UNNEST(@api_key_id::text[]), ''), NULLIF(UNNEST(@model_config_id::uuid[]), '00000000-0000-0000-0000-000000000000'::uuid), NULLIF(UNNEST(@reasoning_effort::text[]), '')::chat_reasoning_effort, UNNEST(@role::chat_message_role[]), @@ -1859,13 +1857,12 @@ RETURNING -- Legacy queue insertion path. When no caller-supplied creator exists, -- preserve the created_by invariant by attributing the queued row to the -- chat owner. -INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, api_key_id, created_by) +INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, created_by) SELECT @chat_id::uuid, @content::jsonb, sqlc.narg('model_config_id')::uuid, sqlc.narg('reasoning_effort')::chat_reasoning_effort, - sqlc.narg('api_key_id')::text, chats.owner_id FROM chats WHERE chats.id = @chat_id::uuid @@ -2865,13 +2862,12 @@ SELECT NOW()::timestamptz AS now; -- Inserts a queued message that carries a position (from the default -- sequence) and an explicit created_by reference. Use this when the -- queued-message creator differs from the chat owner. -INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, api_key_id, created_by) +INSERT INTO chat_queued_messages (chat_id, content, model_config_id, reasoning_effort, created_by) VALUES ( @chat_id::uuid, @content::jsonb, sqlc.narg('model_config_id')::uuid, sqlc.narg('reasoning_effort')::chat_reasoning_effort, - sqlc.narg('api_key_id')::text, @created_by::uuid ) RETURNING *; diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 6ae28da6a91d1..7bf3880955e6a 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -6867,7 +6867,7 @@ func convertChatQueuedMessagePtr(m database.ChatQueuedMessage) *codersdk.ChatQue func convertChatQueuedMessages(msgs []database.ChatQueuedMessage) []codersdk.ChatQueuedMessage { result := make([]codersdk.ChatQueuedMessage, 0, len(msgs)) for _, m := range msgs { - result = append(result, convertChatQueuedMessage(m)) + result = append(result, db2sdk.ChatQueuedMessage(m)) } return result } diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index 58c9d2f48dff6..7a16c26597f29 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -139,15 +139,6 @@ func newChatClientWithAPIAndDatabase(t testing.TB, overrides ...func(*coderdtest return codersdk.NewExperimentalClient(client), api.Database, api } -func currentTestAPIKeyID(t testing.TB, client *codersdk.ExperimentalClient) string { - t.Helper() - - apiKeyID, _, ok := strings.Cut(client.SessionToken(), "-") - require.True(t, ok) - require.NotEmpty(t, apiKeyID) - return apiKeyID -} - func insertTestChatQueuedMessage( ctx context.Context, t testing.TB, @@ -155,10 +146,9 @@ func insertTestChatQueuedMessage( chatID uuid.UUID, content json.RawMessage, modelConfigID uuid.UUID, - apiKeyID string, ) database.ChatQueuedMessage { t.Helper() - return insertTestChatQueuedMessageWithReasoningEffort(ctx, t, db, chatID, content, modelConfigID, apiKeyID, "") + return insertTestChatQueuedMessageWithReasoningEffort(ctx, t, db, chatID, content, modelConfigID, "") } func insertTestChatQueuedMessageWithReasoningEffort( @@ -168,7 +158,6 @@ func insertTestChatQueuedMessageWithReasoningEffort( chatID uuid.UUID, content json.RawMessage, modelConfigID uuid.UUID, - apiKeyID string, reasoningEffort string, ) database.ChatQueuedMessage { t.Helper() @@ -180,7 +169,6 @@ func insertTestChatQueuedMessageWithReasoningEffort( Content: content, ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: modelConfigID != uuid.Nil}, ReasoningEffort: database.NullChatReasoningEffort{ChatReasoningEffort: database.ChatReasoningEffort(reasoningEffort), Valid: reasoningEffort != ""}, - APIKeyID: sql.NullString{String: apiKeyID, Valid: apiKeyID != ""}, }, ) require.NoError(t, err) @@ -5378,11 +5366,9 @@ func TestGetChatUserPrompts(t *testing.T) { t.Helper() content, err := chatprompt.MarshalParts(parts) require.NoError(t, err) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: userID}) msgs, err := db.InsertChatMessages(dbauthz.AsSystemRestricted(ctx), database.InsertChatMessagesParams{ ChatID: chatID, CreatedBy: []uuid.UUID{userID}, - APIKeyID: []string{apiKey.ID}, ModelConfigID: []uuid.UUID{modelConfigID}, Role: []database.ChatMessageRole{database.ChatMessageRoleUser}, ContentVersion: []int16{chatprompt.CurrentContentVersion}, @@ -5489,11 +5475,9 @@ func TestGetChatUserPrompts(t *testing.T) { // without the guard, jsonb_array_elements would raise // "cannot extract elements from a scalar" and the request // would 500. - legacyAPIKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.UserID}) _, err = db.InsertChatMessages(dbauthz.AsSystemRestricted(ctx), database.InsertChatMessagesParams{ ChatID: chat.ID, CreatedBy: []uuid.UUID{user.UserID}, - APIKeyID: []string{legacyAPIKey.ID}, ModelConfigID: []uuid.UUID{modelConfig.ID}, Role: []database.ChatMessageRole{database.ChatMessageRoleUser}, ContentVersion: []int16{chatprompt.ContentVersionV0}, @@ -10301,7 +10285,7 @@ func TestDeleteChatQueuedMessage(t *testing.T) { codersdk.ChatMessageText("queued message for delete route"), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, deleteContent, modelConfig.ID, currentTestAPIKeyID(t, client)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, deleteContent, modelConfig.ID) res, err := client.Request( ctx, @@ -10382,7 +10366,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { codersdk.ChatMessageText(queuedText), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, client)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) promoteRes, err := client.Request( ctx, @@ -10447,7 +10431,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { codersdk.ChatMessageText(queuedText), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, client)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) insertAssistantCostMessage(t, db, chat.ID, modelConfig.ID, 100) @@ -10542,7 +10526,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { codersdk.ChatMessageText("queued message no agents access"), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, client)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) promoteRes, err := memberClient.Request( ctx, @@ -10574,7 +10558,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { codersdk.ChatMessageText("queued"), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, client)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) // Archive the chat. _, err = db.ArchiveChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID) @@ -10664,7 +10648,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { codersdk.ChatMessageText(queuedText), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, client)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) promoteRes, err := client.Request( ctx, @@ -10757,7 +10741,7 @@ func TestPromoteChatQueuedMessage(t *testing.T) { codersdk.ChatMessageText("running-promote"), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, client)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) promoteRes, err := client.Request( ctx, @@ -16035,7 +16019,6 @@ func TestGetChatMessages_Pagination(t *testing.T) { db database.Store, chatID uuid.UUID, modelConfigID uuid.UUID, - apiKeyID string, ) { t.Helper() @@ -16043,7 +16026,7 @@ func TestGetChatMessages_Pagination(t *testing.T) { codersdk.ChatMessageText("queued"), }) require.NoError(t, err) - _ = insertTestChatQueuedMessage(ctx, t, db, chatID, content, modelConfigID, apiKeyID) + _ = insertTestChatQueuedMessage(ctx, t, db, chatID, content, modelConfigID) } t.Run("NoCursorReturnsAllDESCPlusQueued", func(t *testing.T) { @@ -16055,7 +16038,7 @@ func TestGetChatMessages_Pagination(t *testing.T) { modelConfig := createChatModelConfig(t, client) chat, ids := seedChat(t, db, user.UserID, user.OrganizationID, modelConfig.ID, 5) - seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID, currentTestAPIKeyID(t, client)) + seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID) resp, err := client.GetChatMessages(ctx, chat.ID, nil) require.NoError(t, err) @@ -16080,7 +16063,7 @@ func TestGetChatMessages_Pagination(t *testing.T) { modelConfig := createChatModelConfig(t, client) chat, ids := seedChat(t, db, user.UserID, user.OrganizationID, modelConfig.ID, 5) - seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID, currentTestAPIKeyID(t, client)) + seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID) resp, err := client.GetChatMessages(ctx, chat.ID, &codersdk.ChatMessagesPaginationOptions{ BeforeID: ids[2], @@ -16106,7 +16089,7 @@ func TestGetChatMessages_Pagination(t *testing.T) { modelConfig := createChatModelConfig(t, client) chat, ids := seedChat(t, db, user.UserID, user.OrganizationID, modelConfig.ID, 5) - seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID, currentTestAPIKeyID(t, client)) + seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID) resp, err := client.GetChatMessages(ctx, chat.ID, &codersdk.ChatMessagesPaginationOptions{ AfterID: ids[1], @@ -16134,7 +16117,7 @@ func TestGetChatMessages_Pagination(t *testing.T) { modelConfig := createChatModelConfig(t, client) chat, ids := seedChat(t, db, user.UserID, user.OrganizationID, modelConfig.ID, 5) - seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID, currentTestAPIKeyID(t, client)) + seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID) resp, err := client.GetChatMessages(ctx, chat.ID, &codersdk.ChatMessagesPaginationOptions{ AfterID: ids[0], @@ -16163,7 +16146,7 @@ func TestGetChatMessages_Pagination(t *testing.T) { chat, ids := seedChat(t, db, user.UserID, user.OrganizationID, modelConfig.ID, 5) // Seed a queued message so the Empty assertion below verifies // the cursor suppresses queued rows, not just that none exist. - seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID, currentTestAPIKeyID(t, client)) + seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID) resp, err := client.GetChatMessages(ctx, chat.ID, &codersdk.ChatMessagesPaginationOptions{ AfterID: ids[0], @@ -16257,7 +16240,7 @@ func TestGetChatMessages_Pagination(t *testing.T) { chat, ids := seedChat(t, db, user.UserID, user.OrganizationID, modelConfig.ID, 3) // Seed a queued message to prove the cursor path suppresses // it even when nothing else comes back. - seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID, currentTestAPIKeyID(t, client)) + seedQueuedMessage(ctx, t, db, chat.ID, modelConfig.ID) // The steady-state polling case: the caller already has every // message, so after_id equals the largest seen id. The server @@ -16494,12 +16477,12 @@ func TestChatReadOnlySharedWriteHandlers(t *testing.T) { t.Run("PromoteChatQueuedMessage", func(t *testing.T) { t.Parallel() - ctx, ownerClient, sharedClient, chat, db := setup(t) + ctx, _, sharedClient, chat, db := setup(t) queuedContent, err := json.Marshal([]codersdk.ChatMessagePart{ codersdk.ChatMessageText("queued"), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, ownerClient)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) res, err := sharedClient.Request( ctx, @@ -16529,12 +16512,12 @@ func TestChatReadOnlySharedWriteHandlers(t *testing.T) { t.Run("DeleteChatQueuedMessage", func(t *testing.T) { t.Parallel() - ctx, ownerClient, sharedClient, chat, db := setup(t) + ctx, _, sharedClient, chat, db := setup(t) queuedContent, err := json.Marshal([]codersdk.ChatMessagePart{ codersdk.ChatMessageText("queued"), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, ownerClient)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) res, err := sharedClient.Request( ctx, @@ -16679,14 +16662,14 @@ func TestChatOwnerOnlyWriteHandlers(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) - ownerClient, adminClient, chat, db := setupOrgAdminAndOwnerChat(t) + _, adminClient, chat, db := setupOrgAdminAndOwnerChat(t) // Insert a queued message directly in the DB. queuedContent, err := json.Marshal([]codersdk.ChatMessagePart{ codersdk.ChatMessageText("queued"), }) require.NoError(t, err) - queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID, currentTestAPIKeyID(t, ownerClient)) + queuedMessage := insertTestChatQueuedMessage(ctx, t, db, chat.ID, queuedContent, chat.LastModelConfigID) // Org admin tries to promote. promoteRes, err := adminClient.Request( diff --git a/coderd/x/chatd/ARCHITECTURE.md b/coderd/x/chatd/ARCHITECTURE.md index 8b869367e20d5..d07d88b03e9cc 100644 --- a/coderd/x/chatd/ARCHITECTURE.md +++ b/coderd/x/chatd/ARCHITECTURE.md @@ -13,7 +13,7 @@ Chatd attributes AI Gateway requests with a synthetic API key owned by the chat Synthetic keys expire after 30 days. When less than 24 hours remain, chatd extends the expiry of the existing row in place instead of replacing it, because an in-flight generation may have already delegated the current key ID to the gateway. The key ID is therefore stable for the lifetime of the user. Mints and extensions are serialized with a per-user advisory lock, since the partial unique index on token names only covers `login_type = 'token'` rows. The generated token is discarded, so the stored key cannot be used as a bearer credential, and it carries a minimal scope as defense in depth. -The legacy `api_key_id` columns on messages and queued messages are still stamped with the synthetic key for rolling deployment compatibility, but they no longer have foreign keys to `api_keys`. They are not the source of gateway routing. Stale IDs are harmless because chatd resolves attribution from `chats.owner_id`. +Messages and queued messages no longer carry `api_key_id` columns; attribution is resolved solely from `chats.owner_id`. The drop migration discards any IDs stamped by older replicas, and its rollback restores the columns as nullable without backfilling them. Deleting a synthetic key (password reset, explicit key deletion, dbpurge of long-expired keys) does not touch chat messages, queued messages, or their version fields. Chatd mints a replacement on the next request without mutating history. User suspension and deletion still block delegated gateway authorization. diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go index ee24f4a951866..22ba81e1f3b0c 100644 --- a/coderd/x/chatd/chatd.go +++ b/coderd/x/chatd/chatd.go @@ -1286,11 +1286,6 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C return database.Chat{}, limitErr } - apiKeyID, err := p.ensureSyntheticAPIKeyID(ctx, opts.OwnerID) - if err != nil { - return database.Chat{}, xerrors.Errorf("ensure synthetic API key: %w", err) - } - labelsJSON, err := json.Marshal(opts.Labels) if err != nil { return database.Chat{}, xerrors.Errorf("marshal labels: %w", err) @@ -1332,7 +1327,7 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C initialMessages = append(initialMessages, systemMessage(userPromptContent, opts.ModelConfigID)) } initialMessages = append(initialMessages, systemMessage(workspaceAwarenessContent, opts.ModelConfigID)) - initialMessages = append(initialMessages, userMessageWithAPIKeyID(userContent, opts.ModelConfigID, opts.OwnerID, apiKeyID, opts.ReasoningEffort)) + initialMessages = append(initialMessages, userMessage(userContent, opts.ModelConfigID, opts.OwnerID, opts.ReasoningEffort)) if opts.ModelConfigID != uuid.Nil { if err := requireEnabledChatModelConfig(ctx, p.db, opts.ModelConfigID); err != nil { @@ -1418,15 +1413,6 @@ func (p *Server) SendMessage( requestedPlanMode := opts.PlanMode requestedMCPServerIDs := opts.MCPServerIDs - chat, err := p.db.GetChatByID(ctx, opts.ChatID) - if err != nil { - return SendMessageResult{}, xerrors.Errorf("load chat: %w", err) - } - apiKeyID, err := p.ensureSyntheticAPIKeyID(ctx, chat.OwnerID) - if err != nil { - return SendMessageResult{}, xerrors.Errorf("ensure synthetic API key: %w", err) - } - var result SendMessageResult machine := p.newChatMachine(opts.ChatID) updateErr := machine.Update(ctx, func(tx *chatstate.Tx, store database.Store) error { @@ -1491,7 +1477,7 @@ func (p *Server) SendMessage( // Queue capacity is enforced inside tx.SendMessage; this // wrapper only propagates the typed error. sendResult, err := tx.SendMessage(chatstate.SendMessageInput{ - Message: userMessageWithAPIKeyID(content, modelConfigID, messageCreatedBy, apiKeyID, opts.ReasoningEffort), + Message: userMessage(content, modelConfigID, messageCreatedBy, opts.ReasoningEffort), BusyBehavior: busyBehaviorToChatState(busyBehavior), }) if err != nil { @@ -1665,15 +1651,6 @@ func (p *Server) EditMessage( if err != nil { return EditMessageResult{}, xerrors.Errorf("marshal message content: %w", err) } - chat, err := p.db.GetChatByID(ctx, opts.ChatID) - if err != nil { - return EditMessageResult{}, xerrors.Errorf("load chat: %w", err) - } - apiKeyID, err := p.ensureSyntheticAPIKeyID(ctx, chat.OwnerID) - if err != nil { - return EditMessageResult{}, xerrors.Errorf("ensure synthetic API key: %w", err) - } - var ( result EditMessageResult editedMsg database.ChatMessage @@ -1744,7 +1721,6 @@ func (p *Server) EditMessage( Content: content, ModelConfigIDOverride: modelOverride, ReasoningEffortOverride: reasoningEffortOverride, - APIKeyID: sql.NullString{String: apiKeyID, Valid: true}, }) if err != nil { if errors.Is(err, chatstate.ErrEditedMessageNotUser) { @@ -2759,16 +2735,6 @@ type chatMessage struct { runtimeMs int64 } -type userChatMessage struct { - chatMessage - apiKeyID string -} - -func (m userChatMessage) withCreatedBy(id uuid.UUID) userChatMessage { - m.chatMessage = m.chatMessage.withCreatedBy(id) - return m -} - func newChatMessage( role database.ChatMessageRole, content pqtype.NullRawMessage, @@ -2785,25 +2751,6 @@ func newChatMessage( } } -func newUserChatMessage( - apiKeyID string, - content pqtype.NullRawMessage, - visibility database.ChatMessageVisibility, - modelConfigID uuid.UUID, - contentVersion int16, -) userChatMessage { - return userChatMessage{ - chatMessage: newChatMessage( - database.ChatMessageRoleUser, - content, - visibility, - modelConfigID, - contentVersion, - ), - apiKeyID: apiKeyID, - } -} - func (m chatMessage) withCreatedBy(id uuid.UUID) chatMessage { m.createdBy = id return m @@ -2812,10 +2759,8 @@ func (m chatMessage) withCreatedBy(id uuid.UUID) chatMessage { func appendMessageFields( params *database.InsertChatMessagesParams, msg chatMessage, - apiKeyID string, ) { params.CreatedBy = append(params.CreatedBy, msg.createdBy) - params.APIKeyID = append(params.APIKeyID, apiKeyID) params.ModelConfigID = append(params.ModelConfigID, msg.modelConfigID) params.ReasoningEffort = append(params.ReasoningEffort, "") params.Role = append(params.Role, msg.role) @@ -2835,20 +2780,10 @@ func appendMessageFields( } func appendChatMessage(params *database.InsertChatMessagesParams, msg chatMessage) { - if msg.role == database.ChatMessageRoleUser { - panic("developer error: use appendUserChatMessage for user-role messages") - } - appendMessageFields(params, msg, "") -} - -func appendUserChatMessage(params *database.InsertChatMessagesParams, msg userChatMessage) { - appendMessageFields(params, msg.chatMessage, msg.apiKeyID) + appendMessageFields(params, msg) } -// BuildSingleUserChatMessageInsertParams creates batch insert params for -// one user message, requiring an apiKeyID for AI Gateway attribution. -// BuildSingleChatMessageInsertParams creates batch insert params for one -// non-user message using the shared chat message builder. +// BuildSingleChatMessageInsertParams builds insert parameters for one chat message. func BuildSingleChatMessageInsertParams( chatID uuid.UUID, role database.ChatMessageRole, @@ -2865,31 +2800,7 @@ func BuildSingleChatMessageInsertParams( if createdBy != uuid.Nil { msg = msg.withCreatedBy(createdBy) } - if role == database.ChatMessageRoleUser { - appendMessageFields(¶ms, msg, "") - } else { - appendChatMessage(¶ms, msg) - } - return params -} - -func BuildSingleUserChatMessageInsertParams( - chatID uuid.UUID, - apiKeyID string, - content pqtype.NullRawMessage, - visibility database.ChatMessageVisibility, - modelConfigID uuid.UUID, - contentVersion int16, - createdBy uuid.UUID, -) database.InsertChatMessagesParams { - params := database.InsertChatMessagesParams{ //nolint:exhaustruct // Fields populated by appendUserChatMessage. - ChatID: chatID, - } - msg := newUserChatMessage(apiKeyID, content, visibility, modelConfigID, contentVersion) - if createdBy != uuid.Nil { - msg = msg.withCreatedBy(createdBy) - } - appendUserChatMessage(¶ms, msg) + appendChatMessage(¶ms, msg) return params } diff --git a/coderd/x/chatd/chatd_internal_test.go b/coderd/x/chatd/chatd_internal_test.go index 99e78a00d3357..242dccbe1b3a6 100644 --- a/coderd/x/chatd/chatd_internal_test.go +++ b/coderd/x/chatd/chatd_internal_test.go @@ -743,11 +743,6 @@ func TestRenameChatTitle(t *testing.T) { }) } -func withChatMessageAPIKeyID(message database.ChatMessage, apiKeyID string) database.ChatMessage { - message.APIKeyID = sqlNullString(apiKeyID) - return message -} - // requireOutgoingRequestModel asserts that the outgoing request body // requests wantModel. This is so that mock transports can still // verify the outgoing request asked for the expected model. @@ -867,12 +862,12 @@ func TestRegenerateChatTitle_PersistsAndBroadcasts(t *testing.T) { LimitVal: manualTitleMessageWindowLimit, }, ).Return([]database.ChatMessage{ - withChatMessageAPIKeyID(mustChatMessage( + mustChatMessage( t, database.ChatMessageRoleUser, database.ChatMessageVisibilityBoth, codersdk.ChatMessageText(userPrompt), - ), activeAPIKeyID), + ), mustChatMessage( t, database.ChatMessageRoleAssistant, @@ -1019,12 +1014,12 @@ func TestRegenerateChatTitle_SkipsPersistWhenTitleChangedConcurrently(t *testing LimitVal: manualTitleMessageWindowLimit, }, ).Return([]database.ChatMessage{ - withChatMessageAPIKeyID(mustChatMessage( + mustChatMessage( t, database.ChatMessageRoleUser, database.ChatMessageVisibilityBoth, codersdk.ChatMessageText(userPrompt), - ), activeAPIKeyID), + ), }, nil) db.EXPECT().GetChatMessagesByChatIDDescPaginated( gomock.Any(), @@ -3600,7 +3595,6 @@ func TestResolveFallbackModelConfigID(t *testing.T) { OwnerID: uuid.New(), Title: "provider disabled create", ModelConfigID: model.ID, - APIKeyID: "test-api-key-id", InitialUserContent: []codersdk.ChatMessagePart{ codersdk.ChatMessageText("hello"), }, diff --git a/coderd/x/chatd/chatd_test.go b/coderd/x/chatd/chatd_test.go index f89dd2404dd60..b3b74e19ca453 100644 --- a/coderd/x/chatd/chatd_test.go +++ b/coderd/x/chatd/chatd_test.go @@ -74,12 +74,6 @@ type recordedOpenAIRequest struct { ContentLength int64 } -func testAPIKeyID(t testing.TB, db database.Store, userID uuid.UUID) string { - t.Helper() - key, _ := dbgen.APIKey(t, db, database.APIKey{ID: uuid.NewString(), UserID: userID}) - return key.ID -} - func chatAIGatewayTransportFactoryPointer(factory aibridge.TransportFactory) *atomic.Pointer[aibridge.TransportFactory] { var factoryPtr atomic.Pointer[aibridge.TransportFactory] factoryPtr.Store(&factory) @@ -770,7 +764,6 @@ func TestExploreChatUsesPersistedMCPSnapshot(t *testing.T) { codersdk.ChatMessageText("inspect the codebase"), }) require.NoError(t, err) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.ID}) createdExplore, err := chatstate.CreateChat(ctx, db, ps, chatstate.CreateChatInput{ OrganizationID: org.ID, OwnerID: user.ID, @@ -794,7 +787,6 @@ func TestExploreChatUsesPersistedMCPSnapshot(t *testing.T) { ContentVersion: chatprompt.CurrentContentVersion, CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: webSearchModel.ID, Valid: true}, - APIKeyID: sql.NullString{String: apiKey.ID, Valid: true}, }, }, }) @@ -1603,175 +1595,6 @@ func TestUpdateChatHeartbeatsRequiresOwnership(t *testing.T) { require.Equal(t, chat.ID, ids[0]) } -func TestCreateChatPersistsSyntheticAPIKeyIDOnInitialUserMessage(t *testing.T) { - t.Parallel() - - db, ps := dbtestutil.NewDB(t) - replica := newTestServer(t, db, ps, uuid.New()) - - ctx := testutil.Context(t, testutil.WaitLong) - user, org, model := seedChatDependencies(t, db) - chat, err := replica.CreateChat(ctx, chatd.CreateOptions{ - OrganizationID: org.ID, - OwnerID: user.ID, - Title: "create-chat-synthetic-api-key-id", - ModelConfigID: model.ID, - InitialUserContent: []codersdk.ChatMessagePart{codersdk.ChatMessageText("hello")}, - }) - require.NoError(t, err) - - messages, err := db.GetChatMessagesByChatID(ctx, database.GetChatMessagesByChatIDParams{ - ChatID: chat.ID, - AfterID: 0, - }) - require.NoError(t, err) - require.Len(t, messages, 1) - require.Equal(t, database.ChatMessageRoleUser, messages[0].Role) - require.True(t, messages[0].APIKeyID.Valid) - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: chatd.GatewayTokenName(user.ID), - }) - require.NoError(t, err) - require.Equal(t, gatewayKey.ID, messages[0].APIKeyID.String) -} - -func TestSendMessagePersistsSyntheticAPIKeyIDOnUserMessage(t *testing.T) { - t.Parallel() - - db, ps := dbtestutil.NewDB(t) - replica := newTestServer(t, db, ps, uuid.New()) - - ctx := testutil.Context(t, testutil.WaitLong) - user, org, model := seedChatDependencies(t, db) - - chat := dbgen.Chat(t, db, database.Chat{ - OrganizationID: org.ID, - OwnerID: user.ID, - LastModelConfigID: model.ID, - Title: "send-message-synthetic-api-key-id", - }) - - result, err := replica.SendMessage(ctx, chatd.SendMessageOptions{ - ChatID: chat.ID, - CreatedBy: user.ID, - Content: []codersdk.ChatMessagePart{ - codersdk.ChatMessageText("message with synthetic api key id"), - }, - }) - require.NoError(t, err) - require.False(t, result.Queued) - require.True(t, result.Message.APIKeyID.Valid) - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: chatd.GatewayTokenName(user.ID), - }) - require.NoError(t, err) - require.Equal(t, gatewayKey.ID, result.Message.APIKeyID.String) - - stored, err := db.GetChatMessageByID(ctx, result.Message.ID) - require.NoError(t, err) - require.True(t, stored.APIKeyID.Valid) - require.Equal(t, gatewayKey.ID, stored.APIKeyID.String) -} - -func TestSendMessagePersistsSyntheticAPIKeyIDOnQueuedUserMessage(t *testing.T) { - t.Parallel() - - db, ps := dbtestutil.NewDB(t) - replica := newTestServer(t, db, ps, uuid.New()) - - ctx := testutil.Context(t, testutil.WaitLong) - user, org, model := seedChatDependencies(t, db) - chat, err := replica.CreateChat(ctx, chatd.CreateOptions{ - OrganizationID: org.ID, - OwnerID: user.ID, - Title: "queue-synthetic-api-key-id", - ModelConfigID: model.ID, - InitialUserContent: []codersdk.ChatMessagePart{codersdk.ChatMessageText("hello")}, - }) - require.NoError(t, err) - - chat, err = db.UpdateChatStatus(ctx, database.UpdateChatStatusParams{ - ID: chat.ID, - Status: database.ChatStatusRunning, - WorkerID: uuid.NullUUID{UUID: uuid.New(), Valid: true}, - StartedAt: sql.NullTime{Time: time.Now(), Valid: true}, - HeartbeatAt: sql.NullTime{Time: time.Now(), Valid: true}, - }) - require.NoError(t, err) - - result, err := replica.SendMessage(ctx, chatd.SendMessageOptions{ - ChatID: chat.ID, - CreatedBy: user.ID, - Content: []codersdk.ChatMessagePart{codersdk.ChatMessageText("queued")}, - BusyBehavior: chatd.SendMessageBusyBehaviorQueue, - }) - require.NoError(t, err) - require.True(t, result.Queued) - require.NotNil(t, result.QueuedMessage) - - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: chatd.GatewayTokenName(user.ID), - }) - require.NoError(t, err) - require.True(t, result.QueuedMessage.APIKeyID.Valid) - require.Equal(t, gatewayKey.ID, result.QueuedMessage.APIKeyID.String) - - queued, err := db.GetChatQueuedMessages(ctx, chat.ID) - require.NoError(t, err) - require.Len(t, queued, 1) - require.True(t, queued[0].APIKeyID.Valid) - require.Equal(t, gatewayKey.ID, queued[0].APIKeyID.String) -} - -func TestEditMessagePersistsSyntheticAPIKeyIDOnReplacement(t *testing.T) { - t.Parallel() - - db, ps := dbtestutil.NewDB(t) - replica := newTestServer(t, db, ps, uuid.New()) - - ctx := testutil.Context(t, testutil.WaitLong) - user, org, model := seedChatDependencies(t, db) - chat, err := replica.CreateChat(ctx, chatd.CreateOptions{ - OrganizationID: org.ID, - OwnerID: user.ID, - Title: "edit-synthetic-api-key-id", - ModelConfigID: model.ID, - InitialUserContent: []codersdk.ChatMessagePart{codersdk.ChatMessageText("original")}, - }) - require.NoError(t, err) - - messages, err := db.GetChatMessagesByChatID(ctx, database.GetChatMessagesByChatIDParams{ - ChatID: chat.ID, - AfterID: 0, - }) - require.NoError(t, err) - require.Len(t, messages, 1) - - result, err := replica.EditMessage(ctx, chatd.EditMessageOptions{ - ChatID: chat.ID, - EditedMessageID: messages[0].ID, - CreatedBy: user.ID, - Content: []codersdk.ChatMessagePart{codersdk.ChatMessageText("edited")}, - }) - require.NoError(t, err) - - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: chatd.GatewayTokenName(user.ID), - }) - require.NoError(t, err) - require.True(t, result.Message.APIKeyID.Valid) - require.Equal(t, gatewayKey.ID, result.Message.APIKeyID.String) - - stored, err := db.GetChatMessageByID(ctx, result.Message.ID) - require.NoError(t, err) - require.True(t, stored.APIKeyID.Valid) - require.Equal(t, gatewayKey.ID, stored.APIKeyID.String) -} - func TestSendMessageQueueBehaviorQueuesWhenBusy(t *testing.T) { t.Parallel() @@ -2773,7 +2596,6 @@ func TestRecoverStaleRequiresActionChat(t *testing.T) { codersdk.ChatMessageText("hello"), }) require.NoError(t, err) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.ID}) created, err := chatstate.CreateChat(ctx, db, ps, chatstate.CreateChatInput{ OrganizationID: org.ID, OwnerID: user.ID, @@ -2789,7 +2611,6 @@ func TestRecoverStaleRequiresActionChat(t *testing.T) { ContentVersion: chatprompt.CurrentContentVersion, CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: model.ID, Valid: true}, - APIKeyID: sql.NullString{String: apiKey.ID, Valid: true}, }, }, }) @@ -2867,7 +2688,6 @@ func TestNewReplicaRecoversStaleChatFromDeadReplica(t *testing.T) { codersdk.ChatMessageText("hello"), }) require.NoError(t, err) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.ID}) created, err := chatstate.CreateChat(ctx, db, ps, chatstate.CreateChatInput{ OrganizationID: org.ID, OwnerID: user.ID, @@ -2882,7 +2702,6 @@ func TestNewReplicaRecoversStaleChatFromDeadReplica(t *testing.T) { ContentVersion: chatprompt.CurrentContentVersion, CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: model.ID, Valid: true}, - APIKeyID: sql.NullString{String: apiKey.ID, Valid: true}, }, }, }) @@ -5459,11 +5278,6 @@ func TestActiveServer_RoutingPreservesAPIKeyAfterCompaction(t *testing.T) { }, }) require.NoError(t, err) - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: chatd.GatewayTokenName(user.ID), - }) - require.NoError(t, err) contextContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{{ Type: codersdk.ChatMessagePartTypeContextFile, ContextFileAgentID: uuid.NullUUID{UUID: dbAgent.ID, Valid: true}, @@ -5473,9 +5287,9 @@ func TestActiveServer_RoutingPreservesAPIKeyAfterCompaction(t *testing.T) { ContextFileDirectory: "/home/coder/project", }}) require.NoError(t, err) - _, err = db.InsertChatMessages(ctx, chatd.BuildSingleUserChatMessageInsertParams( + _, err = db.InsertChatMessages(ctx, chatd.BuildSingleChatMessageInsertParams( chat.ID, - gatewayKey.ID, + database.ChatMessageRoleUser, contextContent, database.ChatMessageVisibilityBoth, model.ID, @@ -5497,14 +5311,17 @@ func TestActiveServer_RoutingPreservesAPIKeyAfterCompaction(t *testing.T) { chatResult := waitForTerminalChat(ctx, t, db, chat.ID) require.Equal(t, database.ChatStatusWaiting, chatResult.Status) require.False(t, chatResult.LastError.Valid) + gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ + UserID: user.ID, + TokenName: chatd.GatewayTokenName(user.ID), + }) + require.NoError(t, err) messages := chatMessages(ctx, t, db, chat.ID) promptMessages, err := db.GetChatMessagesForPromptByChatID(ctx, chat.ID) require.NoError(t, err) compressed := compressedChatSummarizedMessages(t, append(promptMessages, messages...)) require.Len(t, compressed.summaries, 1) - require.True(t, compressed.summaries[0].APIKeyID.Valid) - require.Equal(t, gatewayKey.ID, compressed.summaries[0].APIKeyID.String) requests := factory.RequestsSnapshot() require.NotEmpty(t, requests) @@ -6779,7 +6596,6 @@ func userMessageForTest( ContentVersion: chatprompt.CurrentContentVersion, ModelConfigID: uuid.NullUUID{UUID: modelID, Valid: true}, CreatedBy: uuid.NullUUID{UUID: createdBy, Valid: true}, - APIKeyID: sql.NullString{String: apiKeyID, Valid: apiKeyID != ""}, } } @@ -8357,29 +8173,15 @@ func insertChatMessageParts( t.Helper() content, err := chatprompt.MarshalParts(parts) require.NoError(t, err) - var params database.InsertChatMessagesParams - if role == database.ChatMessageRoleUser { - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: createdBy}) - params = chatd.BuildSingleUserChatMessageInsertParams( - chatID, - apiKey.ID, - content, - database.ChatMessageVisibilityBoth, - modelID, - chatprompt.CurrentContentVersion, - createdBy, - ) - } else { - params = chatd.BuildSingleChatMessageInsertParams( - chatID, - role, - content, - database.ChatMessageVisibilityBoth, - modelID, - chatprompt.CurrentContentVersion, - createdBy, - ) - } + params := chatd.BuildSingleChatMessageInsertParams( + chatID, + role, + content, + database.ChatMessageVisibilityBoth, + modelID, + chatprompt.CurrentContentVersion, + createdBy, + ) messages, err := db.InsertChatMessages(ctx, params) require.NoError(t, err) require.Len(t, messages, 1) @@ -10211,12 +10013,11 @@ func seedAIGatewayOpenAITestDependencies( t *testing.T, db database.Store, openAIURL string, -) (database.User, database.Organization, database.AIProvider, database.ChatModelConfig, database.APIKey) { +) (database.User, database.Organization, database.AIProvider, database.ChatModelConfig) { t.Helper() user := dbgen.User(t, db, database.User{}) org := dbgen.Organization(t, db, database.Organization{}) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.ID}) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: user.ID, OrganizationID: org.ID, @@ -10239,7 +10040,7 @@ func seedAIGatewayOpenAITestDependencies( }) require.NoError(t, err) - return user, org, provider, model, apiKey + return user, org, provider, model } func TestProcessChat_RoutingUsesDelegatedAPIKey(t *testing.T) { @@ -10258,7 +10059,7 @@ func TestProcessChat_RoutingUsesDelegatedAPIKey(t *testing.T) { }) factory := chattest.NewMockAIBridgeTransport(t, openAIURL) - user, org, provider, model, _ := seedAIGatewayOpenAITestDependencies(t, db, openAIURL) + user, org, provider, model := seedAIGatewayOpenAITestDependencies(t, db, openAIURL) creator := newTestServer(t, db, ps, uuid.New()) chat, err := creator.CreateChat(ctx, chatd.CreateOptions{ @@ -10271,11 +10072,6 @@ func TestProcessChat_RoutingUsesDelegatedAPIKey(t *testing.T) { }, }) require.NoError(t, err) - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: chatd.GatewayTokenName(user.ID), - }) - require.NoError(t, err) _, events, cancel, ok := creator.Subscribe(ctx, chat.ID, nil, 0) require.True(t, ok) @@ -10292,6 +10088,11 @@ func TestProcessChat_RoutingUsesDelegatedAPIKey(t *testing.T) { chatResult := waitForTerminalChat(ctx, t, db, chat.ID) require.Equal(t, database.ChatStatusWaiting, chatResult.Status) require.False(t, chatResult.LastError.Valid) + gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ + UserID: user.ID, + TokenName: chatd.GatewayTokenName(user.ID), + }) + require.NoError(t, err) requests := factory.RequestsSnapshot() require.NotEmpty(t, requests) @@ -10324,7 +10125,7 @@ func TestProcessChat_RoutingPreservesAPIKeyAfterWorkspaceContext(t *testing.T) { return chattest.OpenAINonStreamingResponse(`{"title":"AI Gateway Workspace"}`) }) factory := chattest.NewMockAIBridgeTransport(t, openAIURL) - user, org, provider, model, _ := seedAIGatewayOpenAITestDependencies(t, db, openAIURL) + user, org, provider, model := seedAIGatewayOpenAITestDependencies(t, db, openAIURL) ws, dbAgent := seedWorkspaceWithAgent(t, db, user.ID) creator := newTestServer(t, db, ps, uuid.New()) @@ -10339,11 +10140,6 @@ func TestProcessChat_RoutingPreservesAPIKeyAfterWorkspaceContext(t *testing.T) { }, }) require.NoError(t, err) - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: chatd.GatewayTokenName(user.ID), - }) - require.NoError(t, err) const contextText = "# Project instructions\nAlways keep routing metadata." // Workspace context is sourced from the agent's pinned snapshot. Seed it so @@ -10374,6 +10170,11 @@ func TestProcessChat_RoutingPreservesAPIKeyAfterWorkspaceContext(t *testing.T) { pinned, err := db.ListChatContextResourcesByChatID(ctx, chat.ID) require.NoError(t, err) require.NotEmpty(t, pinned, "workspace context should be pinned to the chat") + gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ + UserID: user.ID, + TokenName: chatd.GatewayTokenName(user.ID), + }) + require.NoError(t, err) requests := factory.RequestsSnapshot() require.NotEmpty(t, requests) @@ -12189,7 +11990,6 @@ func TestPromoteQueuedPreservesReasoningEffort(t *testing.T) { Content: content.RawMessage, ModelConfigID: uuid.NullUUID{UUID: model.ID, Valid: true}, ReasoningEffort: database.NullChatReasoningEffort{ChatReasoningEffort: database.ChatReasoningEffortHigh, Valid: true}, - APIKeyID: sql.NullString{String: testAPIKeyID(t, db, user.ID), Valid: true}, CreatedBy: user.ID, }) require.NoError(t, err) diff --git a/coderd/x/chatd/chatstate/machine_test.go b/coderd/x/chatd/chatstate/machine_test.go index c4def781742ba..e89effc9cef89 100644 --- a/coderd/x/chatd/chatstate/machine_test.go +++ b/coderd/x/chatd/chatstate/machine_test.go @@ -2,7 +2,6 @@ package chatstate_test import ( "context" - "database/sql" "encoding/json" "slices" "sync" @@ -34,13 +33,6 @@ type testFixture struct { User database.User Org database.Organization Model database.ChatModelConfig - APIKey database.APIKey -} - -// apiKeyID returns the fixture API key wrapped for the chatstate -// inputs that require a non-null api_key_id (for example EditMessage). -func (f *testFixture) apiKeyID() sql.NullString { - return sql.NullString{String: f.APIKey.ID, Valid: true} } func newTestFixture(t *testing.T) *testFixture { @@ -60,7 +52,6 @@ func newTestFixture(t *testing.T) *testFixture { model := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{ IsDefault: true, }) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.ID}) pub := newRecordingPubsub() return &testFixture{ DB: db, @@ -69,7 +60,6 @@ func newTestFixture(t *testing.T) *testFixture { User: user, Org: org, Model: model, - APIKey: apiKey, } } diff --git a/coderd/x/chatd/chatstate/messages.go b/coderd/x/chatd/chatstate/messages.go index 50d84563f22e4..b867c1f05eac8 100644 --- a/coderd/x/chatd/chatstate/messages.go +++ b/coderd/x/chatd/chatstate/messages.go @@ -35,7 +35,6 @@ type Message struct { ContextLimit sql.NullInt64 TotalCostMicros sql.NullInt64 RuntimeMs sql.NullInt64 - APIKeyID sql.NullString } // toInsertParams converts a batch of Messages into the parallel-array @@ -51,7 +50,6 @@ func toInsertParams(chatID uuid.UUID, messages []Message) database.InsertChatMes CreatedBy: make([]uuid.UUID, n), ModelConfigID: make([]uuid.UUID, n), ReasoningEffort: make([]string, n), - APIKeyID: make([]string, n), Role: make([]database.ChatMessageRole, n), Content: make([]string, n), ContentVersion: make([]int16, n), @@ -73,9 +71,6 @@ func toInsertParams(chatID uuid.UUID, messages []Message) database.InsertChatMes if m.ReasoningEffort.Valid { params.ReasoningEffort[i] = string(m.ReasoningEffort.ChatReasoningEffort) } - if m.APIKeyID.Valid { - params.APIKeyID[i] = m.APIKeyID.String - } params.Role[i] = m.Role if m.Content.Valid { params.Content[i] = string(m.Content.RawMessage) diff --git a/coderd/x/chatd/chatstate/synthetic_cancellation_test.go b/coderd/x/chatd/chatstate/synthetic_cancellation_test.go index 75880aa2f7f56..d4056c34929c8 100644 --- a/coderd/x/chatd/chatstate/synthetic_cancellation_test.go +++ b/coderd/x/chatd/chatstate/synthetic_cancellation_test.go @@ -249,7 +249,6 @@ func testEditMessageSynthesizesToolCancellationsBeforeReplacement(t *testing.T) MessageID: secondUserID, CreatedBy: f.User.ID, Content: editedContent, - APIKeyID: f.apiKeyID(), }) return err })) diff --git a/coderd/x/chatd/chatstate/transitions.go b/coderd/x/chatd/chatstate/transitions.go index 3b2f23479c175..86f57e1f43464 100644 --- a/coderd/x/chatd/chatstate/transitions.go +++ b/coderd/x/chatd/chatstate/transitions.go @@ -230,14 +230,17 @@ func (tx *Tx) insertQueuedMessage(ownerFallback uuid.UUID, m Message) (database. if err := tx.requireQueueCapacity(); err != nil { return database.ChatQueuedMessage{}, err } - return tx.store.InsertChatQueuedMessageWithCreator(tx.ctx, database.InsertChatQueuedMessageWithCreatorParams{ + row, err := tx.store.InsertChatQueuedMessageWithCreator(tx.ctx, database.InsertChatQueuedMessageWithCreatorParams{ ChatID: tx.chatID, Content: rawContent, ModelConfigID: m.ModelConfigID, ReasoningEffort: m.ReasoningEffort, CreatedBy: createdBy, - APIKeyID: m.APIKeyID, }) + if err != nil { + return database.ChatQueuedMessage{}, err + } + return row, nil } // messageFromQueuedRow synthesizes a Message from a stored queued row, @@ -251,7 +254,6 @@ func messageFromQueuedRow(q database.ChatQueuedMessage) Message { ReasoningEffort: q.ReasoningEffort, CreatedBy: uuid.NullUUID{UUID: q.CreatedBy, Valid: true}, ContentVersion: chatprompt.CurrentContentVersion, - APIKeyID: q.APIKeyID, } } @@ -416,11 +418,12 @@ func (tx *Tx) sendMessageE1(chat database.Chat, m Message) (SendMessageResult, e if err != nil { return SendMessageResult{}, xerrors.Errorf("get queue head: %w", err) } + headMessage := head cancels, err := synthesizePendingToolCancellations(tx.ctx, tx.store, chat, "Tool execution interrupted by queued message promotion", false) if err != nil { return SendMessageResult{}, err } - promoted := messageFromQueuedRow(head) + promoted := messageFromQueuedRow(headMessage) inserted, err := tx.insertMessages(append(cancels, promoted)) if err != nil { return SendMessageResult{}, xerrors.Errorf("insert promoted queued head: %w", err) @@ -491,7 +494,6 @@ type EditMessageInput struct { Content pqtype.NullRawMessage ModelConfigIDOverride uuid.NullUUID ReasoningEffortOverride database.NullChatReasoningEffort - APIKeyID sql.NullString } // EditMessageResult is returned by [Tx.EditMessage]. @@ -571,10 +573,6 @@ func (tx *Tx) EditMessage(input EditMessageInput) (EditMessageResult, error) { if input.ReasoningEffortOverride.Valid { reasoningEffort = input.ReasoningEffortOverride } - apiKeyID := input.APIKeyID - if !apiKeyID.Valid { - return EditMessageResult{}, xerrors.Errorf("api_key_id is required") - } replacement := Message{ Role: database.ChatMessageRoleUser, Content: input.Content, @@ -583,7 +581,6 @@ func (tx *Tx) EditMessage(input EditMessageInput) (EditMessageResult, error) { ReasoningEffort: reasoningEffort, CreatedBy: uuid.NullUUID{UUID: input.CreatedBy, Valid: true}, ContentVersion: chatprompt.CurrentContentVersion, - APIKeyID: apiKeyID, } insertedReplacement, err := tx.insertMessages([]Message{replacement}) if err != nil { @@ -643,6 +640,7 @@ func (tx *Tx) DeleteQueuedMessage(input DeleteQueuedMessageInput) (DeleteQueuedM if err != nil { return DeleteQueuedMessageResult{}, xerrors.Errorf("get queued: %w", err) } + targetMessage := target rows, err := tx.store.DeleteChatQueuedMessageReturningCount(tx.ctx, database.DeleteChatQueuedMessageReturningCountParams{ ID: input.QueuedMessageID, ChatID: tx.chatID, @@ -654,7 +652,7 @@ func (tx *Tx) DeleteQueuedMessage(input DeleteQueuedMessageInput) (DeleteQueuedM return DeleteQueuedMessageResult{}, ErrQueuedMessageNotFound } return DeleteQueuedMessageResult{ - DeletedQueuedMessage: target, + DeletedQueuedMessage: targetMessage, }, nil } @@ -688,6 +686,7 @@ func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueu if err != nil { return PromoteQueuedMessageResult{}, xerrors.Errorf("get queued: %w", err) } + targetMessage := target rows, err := tx.store.ReorderChatQueuedMessageToHead(tx.ctx, database.ReorderChatQueuedMessageToHeadParams{ ID: input.QueuedMessageID, ChatID: tx.chatID, @@ -713,7 +712,7 @@ func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueu return PromoteQueuedMessageResult{}, xerrors.Errorf("set interrupting: %w", err) } return PromoteQueuedMessageResult{ - QueuedMessage: target, + QueuedMessage: targetMessage, ReorderedQueueOnly: reorderOnly, }, nil } @@ -726,7 +725,7 @@ func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueu if err != nil { return PromoteQueuedMessageResult{}, err } - promotedMsg := messageFromQueuedRow(target) + promotedMsg := messageFromQueuedRow(targetMessage) inserted, err := tx.insertMessages(append(cancels, promotedMsg)) if err != nil { return PromoteQueuedMessageResult{}, xerrors.Errorf("insert promoted queued message: %w", err) @@ -756,7 +755,7 @@ func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueu cancellations := inserted[:len(inserted)-1] insertedUserMsg := inserted[len(inserted)-1] return PromoteQueuedMessageResult{ - QueuedMessage: target, + QueuedMessage: targetMessage, InsertedMessage: &insertedUserMsg, CancellationMessages: cancellations, ReorderedQueueOnly: reorderOnly, @@ -1211,7 +1210,8 @@ func (tx *Tx) FinishInterruption(input FinishInterruptionInput) (FinishInterrupt if err != nil { return FinishInterruptionResult{}, xerrors.Errorf("get queue head: %w", err) } - promotedMsg := messageFromQueuedRow(head) + headMessage := head + promotedMsg := messageFromQueuedRow(headMessage) insertedHead, err := tx.insertMessages([]Message{promotedMsg}) if err != nil { return FinishInterruptionResult{}, xerrors.Errorf("insert promoted queue head: %w", err) @@ -1277,11 +1277,12 @@ func (tx *Tx) FinishTurn(_ FinishTurnInput) (FinishTurnResult, error) { if err != nil { return FinishTurnResult{}, xerrors.Errorf("get queue head: %w", err) } + headMessage := head cancels, err := synthesizePendingToolCancellations(tx.ctx, tx.store, chat, "Tool execution interrupted by queued message promotion", false) if err != nil { return FinishTurnResult{}, err } - promotedMsg := messageFromQueuedRow(head) + promotedMsg := messageFromQueuedRow(headMessage) inserted, err := tx.insertMessages(append(cancels, promotedMsg)) if err != nil { return FinishTurnResult{}, xerrors.Errorf("insert promoted queue head: %w", err) diff --git a/coderd/x/chatd/chatstate/transitions_helpers_test.go b/coderd/x/chatd/chatstate/transitions_helpers_test.go index 91cc2419f8319..c44e5af1647df 100644 --- a/coderd/x/chatd/chatstate/transitions_helpers_test.go +++ b/coderd/x/chatd/chatstate/transitions_helpers_test.go @@ -794,9 +794,14 @@ func assertChatMessageText(t *testing.T, msg database.ChatMessage, want string) // matrix cases that need to verify the body inserted into // chat_queued_messages via SendMessage. func assertQueuedMessageText(t *testing.T, queued database.ChatQueuedMessage, want string) { + t.Helper() + assertQueuedMessageContent(t, queued.Content, want) +} + +func assertQueuedMessageContent(t *testing.T, content json.RawMessage, want string) { t.Helper() var parts []codersdk.ChatMessagePart - require.NoError(t, json.Unmarshal(queued.Content, &parts), "unmarshal queued content") + require.NoError(t, json.Unmarshal(content, &parts), "unmarshal queued content") require.Len(t, parts, 1, "expected exactly one queued content part") require.Equal(t, codersdk.ChatMessagePartTypeText, parts[0].Type, "expected a text content part") @@ -814,7 +819,7 @@ func assertQueueBodiesInOrder(ctx context.Context, t *testing.T, f *testFixture, require.NoError(t, err) require.Len(t, rows, len(want), "queue length must match expected bodies") for i, r := range rows { - assertQueuedMessageText(t, r, want[i]) + assertQueuedMessageContent(t, r.Content, want[i]) } } diff --git a/coderd/x/chatd/chatstate/transitions_matrix_test.go b/coderd/x/chatd/chatstate/transitions_matrix_test.go index ef35090f7dad2..cc2e579382170 100644 --- a/coderd/x/chatd/chatstate/transitions_matrix_test.go +++ b/coderd/x/chatd/chatstate/transitions_matrix_test.go @@ -136,7 +136,6 @@ func applyEditMessage(t *testing.T, f *testFixture, tx *chatstate.Tx, seeded see MessageID: seeded.initialUserMessageID, CreatedBy: f.User.ID, Content: content, - APIKeyID: f.apiKeyID(), }) return err } diff --git a/coderd/x/chatd/chatstate_bridge.go b/coderd/x/chatd/chatstate_bridge.go index 04aae64c07363..ae3cf59e11403 100644 --- a/coderd/x/chatd/chatstate_bridge.go +++ b/coderd/x/chatd/chatstate_bridge.go @@ -1,8 +1,6 @@ package chatd import ( - "database/sql" - "github.com/google/uuid" "github.com/sqlc-dev/pqtype" @@ -29,7 +27,7 @@ func systemMessage(rawContent pqtype.NullRawMessage, modelConfigID uuid.UUID) ch } } -func userMessageWithAPIKeyID(rawContent pqtype.NullRawMessage, modelConfigID, createdBy uuid.UUID, apiKeyID string, reasoningEffort *string) chatstate.Message { +func userMessage(rawContent pqtype.NullRawMessage, modelConfigID, createdBy uuid.UUID, reasoningEffort *string) chatstate.Message { var effort database.NullChatReasoningEffort if reasoningEffort != nil && *reasoningEffort != "" { effort = database.NullChatReasoningEffort{ChatReasoningEffort: database.ChatReasoningEffort(*reasoningEffort), Valid: true} @@ -42,7 +40,6 @@ func userMessageWithAPIKeyID(rawContent pqtype.NullRawMessage, modelConfigID, cr ReasoningEffort: effort, CreatedBy: uuid.NullUUID{UUID: createdBy, Valid: createdBy != uuid.Nil}, ContentVersion: chatprompt.CurrentContentVersion, - APIKeyID: sql.NullString{String: apiKeyID, Valid: apiKeyID != ""}, } } diff --git a/coderd/x/chatd/generation.go b/coderd/x/chatd/generation.go index b9c43d8063f97..9dad61bb87c5c 100644 --- a/coderd/x/chatd/generation.go +++ b/coderd/x/chatd/generation.go @@ -740,7 +740,6 @@ func (s *taskStarter) generateCompaction( } messages, err := buildCompactionMessages(buildCompactionMessagesInput{ modelConfigID: prepared.ModelConfigID, - activeAPIKeyID: prepared.ModelBuildOptions.ActiveAPIKeyID, toolCallID: compactionOpts.ToolCallID, toolName: compactionOpts.ToolName, compaction: compactionOutcome(outcome), diff --git a/coderd/x/chatd/generation_preparer_internal_test.go b/coderd/x/chatd/generation_preparer_internal_test.go index 13cbb4ac13b0d..0004da960bc8c 100644 --- a/coderd/x/chatd/generation_preparer_internal_test.go +++ b/coderd/x/chatd/generation_preparer_internal_test.go @@ -1,7 +1,6 @@ package chatd //nolint:testpackage // Exercises unexported re-derivation helpers. import ( - "database/sql" "encoding/json" "testing" @@ -93,7 +92,6 @@ func TestPrepareGenerationClampsRequestedReasoningEffortToMax(t *testing.T) { db, ps := dbtestutil.NewDB(t) ctx := chatdTestContext(t) user := dbgen.User(t, db, database.User{}) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.ID}) org := dbgen.Organization(t, db, database.Organization{}) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: user.ID, @@ -135,7 +133,6 @@ func TestPrepareGenerationClampsRequestedReasoningEffortToMax(t *testing.T) { }, CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true}, ContentVersion: chatprompt.CurrentContentVersion, - APIKeyID: sql.NullString{String: apiKey.ID, Valid: true}, }, }, }) @@ -161,6 +158,74 @@ func TestPrepareGenerationClampsRequestedReasoningEffortToMax(t *testing.T) { require.Equal(t, fantasyopenai.ReasoningEffortMedium, *providerOptions.ReasoningEffort) } +func TestPrepareGenerationSubagentUsesOwnerSyntheticAPIKey(t *testing.T) { + t.Parallel() + + db, ps := dbtestutil.NewDB(t) + ctx := chatdTestContext(t) + user := dbgen.User(t, db, database.User{}) + org := dbgen.Organization(t, db, database.Organization{}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + UserID: user.ID, + OrganizationID: org.ID, + }) + provider := dbgen.AIProviderWithOptionalKey(t, db, database.AIProvider{ + Type: database.AIProviderTypeOpenai, + }, "test-key") + modelConfig := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{ + Model: "gpt-4o-mini", + AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true}, + }, func(p *database.InsertChatModelConfigParams) { + p.Enabled = true + }) + parent := dbgen.Chat(t, db, database.Chat{ + OrganizationID: org.ID, + OwnerID: user.ID, + LastModelConfigID: modelConfig.ID, + }) + created, err := chatstate.CreateChat(ctx, db, ps, chatstate.CreateChatInput{ + OrganizationID: org.ID, + OwnerID: user.ID, + ParentChatID: uuid.NullUUID{UUID: parent.ID, Valid: true}, + RootChatID: uuid.NullUUID{UUID: parent.ID, Valid: true}, + LastModelConfigID: modelConfig.ID, + Title: "subagent attribution", + ClientType: database.ChatClientTypeApi, + InitialMessages: []chatstate.Message{ + { + Role: database.ChatMessageRoleUser, + Content: mustMarshalText(t, "inspect the workspace"), + Visibility: database.ChatMessageVisibilityBoth, + ModelConfigID: uuid.NullUUID{UUID: modelConfig.ID, Valid: true}, + CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true}, + ContentVersion: chatprompt.CurrentContentVersion, + }, + }, + }) + require.NoError(t, err) + + server := newInternalTestServer( + t, + db, + ps, + chatprovider.ProviderAPIKeys{}, + withInternalTestServerTransportFactory(&aibridgeTestFactory{}), + ) + prepared, err := server.prepareGeneration(ctx, generationPrepareInput{ + Chat: created.Chat, + Messages: created.InitialMessages, + }) + require.NoError(t, err) + t.Cleanup(prepared.Cleanup) + + gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ + UserID: user.ID, + TokenName: GatewayTokenName(user.ID), + }) + require.NoError(t, err) + require.Equal(t, gatewayKey.ID, prepared.ModelBuildOptions.ActiveAPIKeyID) +} + // TestDeriveFinalTurnRunResult exercises the re-derivation path that replaces // the old in-memory generationSideEffects stash. The server here never ran // prepareGeneration, so a passing test proves the finish-turn inputs are @@ -196,7 +261,6 @@ func TestDeriveFinalTurnRunResult(t *testing.T) { p.Enabled = true p.IsDefault = true }) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.ID}) created, err := chatstate.CreateChat(ctx, db, ps, chatstate.CreateChatInput{ OrganizationID: org.ID, @@ -212,7 +276,6 @@ func TestDeriveFinalTurnRunResult(t *testing.T) { ContentVersion: chatprompt.CurrentContentVersion, CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: modelCfg.ID, Valid: true}, - APIKeyID: sql.NullString{String: apiKey.ID, Valid: true}, }, }, }) @@ -314,7 +377,6 @@ func TestDeriveFinalTurnRunResult(t *testing.T) { DisplayName: "gpt-4o-mini", AIProviderID: uuid.NullUUID{UUID: provider.ID, Valid: true}, }) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: user.ID}) created, err := chatstate.CreateChat(ctx, db, ps, chatstate.CreateChatInput{ OrganizationID: org.ID, @@ -330,7 +392,6 @@ func TestDeriveFinalTurnRunResult(t *testing.T) { ContentVersion: chatprompt.CurrentContentVersion, CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: modelCfg.ID, Valid: true}, - APIKeyID: sql.NullString{String: apiKey.ID, Valid: true}, }, }, }) diff --git a/coderd/x/chatd/helpers_test.go b/coderd/x/chatd/helpers_test.go index 178c51b94612e..18e4ec3f0ecbc 100644 --- a/coderd/x/chatd/helpers_test.go +++ b/coderd/x/chatd/helpers_test.go @@ -202,7 +202,6 @@ func userTextMessage(t *testing.T, text string, createdBy uuid.UUID, modelConfig ContentVersion: chatprompt.CurrentContentVersion, CreatedBy: uuid.NullUUID{UUID: createdBy, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true}, - APIKeyID: sql.NullString{String: apiKeyID, Valid: apiKeyID != ""}, } } diff --git a/coderd/x/chatd/message_conversion.go b/coderd/x/chatd/message_conversion.go index c9d4cc348e67a..2197e0aaab445 100644 --- a/coderd/x/chatd/message_conversion.go +++ b/coderd/x/chatd/message_conversion.go @@ -255,7 +255,6 @@ func textFromParts(parts []codersdk.ChatMessagePart) string { type buildCompactionMessagesInput struct { modelConfigID uuid.UUID - activeAPIKeyID string toolCallID string toolName string compaction compactionOutcome @@ -319,7 +318,6 @@ func buildCompactionMessages(input buildCompactionMessagesInput) (compactionMess Visibility: database.ChatMessageVisibilityModel, ModelConfigID: uuid.NullUUID{UUID: input.modelConfigID, Valid: input.modelConfigID != uuid.Nil}, ContentVersion: contentVersion, - APIKeyID: sql.NullString{String: input.activeAPIKeyID, Valid: input.activeAPIKeyID != ""}, }, baseMessage(database.ChatMessageRoleAssistant, database.ChatMessageVisibilityUser, input.modelConfigID, contentVersion, assistantContent), baseMessage(database.ChatMessageRoleTool, database.ChatMessageVisibilityBoth, input.modelConfigID, contentVersion, toolContent), diff --git a/coderd/x/chatd/model_routing_internal_test.go b/coderd/x/chatd/model_routing_internal_test.go index e6d9103435f82..730522e6b9435 100644 --- a/coderd/x/chatd/model_routing_internal_test.go +++ b/coderd/x/chatd/model_routing_internal_test.go @@ -365,10 +365,6 @@ func TestAIGatewayModelForwardsProviderAuth(t *testing.T) { }) } -func sqlNullString(value string) sql.NullString { - return sql.NullString{String: value, Valid: value != ""} -} - func TestAIBridgeRoutingFailClosed(t *testing.T) { t.Parallel() diff --git a/coderd/x/chatd/stream_loop.go b/coderd/x/chatd/stream_loop.go index 5004e8a490ea8..06971c0e305bb 100644 --- a/coderd/x/chatd/stream_loop.go +++ b/coderd/x/chatd/stream_loop.go @@ -176,10 +176,11 @@ func (l *streamLoop) loadDBSnapshot(ctx context.Context) (streamDBSnapshot, erro if chat.QueueVersion > l.state.queueVersion { snapshot.queueChanged = true - snapshot.queue, err = tx.GetChatQueuedMessages(ctx, l.chatID) + queued, err := tx.GetChatQueuedMessages(ctx, l.chatID) if err != nil { return xerrors.Errorf("get chat queue: %w", err) } + snapshot.queue = queued } if chat.Status == database.ChatStatusRequiresAction { diff --git a/coderd/x/chatd/subagent.go b/coderd/x/chatd/subagent.go index f2c50e3023cc0..e0d0cc8176de9 100644 --- a/coderd/x/chatd/subagent.go +++ b/coderd/x/chatd/subagent.go @@ -1056,11 +1056,6 @@ func (p *Server) createChildSubagentChatWithOptions( if modelConfigID == uuid.Nil { return database.Chat{}, xerrors.New("model config is required") } - childAPIKeyID, err := p.ensureSyntheticAPIKeyID(ctx, parent.OwnerID) - if err != nil { - return database.Chat{}, xerrors.Errorf("ensure synthetic API key: %w", err) - } - childPlanMode := parent.PlanMode if opts.planModeOverride != nil { childPlanMode = *opts.planModeOverride @@ -1131,7 +1126,7 @@ func (p *Server) createChildSubagentChatWithOptions( // workspace context the same way a top-level chat does: pinned from the // agent's latest snapshot (see hydrateChatContextOnCreate below). The // parent's context is not copied into child history. - initialMessages = append(initialMessages, userMessageWithAPIKeyID(userContent, modelConfigID, parent.OwnerID, childAPIKeyID, opts.reasoningEffortOverride)) + initialMessages = append(initialMessages, userMessage(userContent, modelConfigID, parent.OwnerID, opts.reasoningEffortOverride)) publisher := p.pubsub if publisher == nil { diff --git a/coderd/x/chatd/subagent_internal_test.go b/coderd/x/chatd/subagent_internal_test.go index 896d658106dbe..ea7cd3facf1f8 100644 --- a/coderd/x/chatd/subagent_internal_test.go +++ b/coderd/x/chatd/subagent_internal_test.go @@ -651,89 +651,6 @@ func upsertInternalUserChatPersonalModelOverride( ) } -func TestCreateChildSubagentChatPersistsOwnerSyntheticAPIKeyID(t *testing.T) { - t.Parallel() - - db, ps := dbtestutil.NewDB(t) - server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{}) - - ctx := chatdTestContext(t) - user, org, model := seedInternalChatDeps(t, db) - parent := createInternalParentChat( - ctx, t, server, db, org.ID, user.ID, model.ID, "parent-child-key", - ) - - child, err := server.createChildSubagentChatWithOptions( - ctx, - parent, - "inspect the workspace", - "", - childSubagentChatOptions{}, - ) - require.NoError(t, err) - - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: GatewayTokenName(user.ID), - }) - require.NoError(t, err) - messages, err := db.GetChatMessagesByChatID(ctx, database.GetChatMessagesByChatIDParams{ - ChatID: child.ID, - AfterID: 0, - }) - require.NoError(t, err) - for _, message := range messages { - if message.Role != database.ChatMessageRoleUser { - continue - } - require.True(t, message.APIKeyID.Valid) - require.Equal(t, gatewayKey.ID, message.APIKeyID.String) - return - } - require.Fail(t, "child user message not found") -} - -func TestSendSubagentMessagePersistsOwnerSyntheticAPIKeyID(t *testing.T) { - t.Parallel() - - db, ps := dbtestutil.NewDB(t) - server := newInternalTestServer(t, db, ps, chatprovider.ProviderAPIKeys{}) - - ctx := chatdTestContext(t) - user, org, model := seedInternalChatDeps(t, db) - parent, child := createParentChildChats(ctx, t, server, user, org, model) - setChatStatus(ctx, t, db, child.ID, database.ChatStatusWaiting, "") - - _, err := server.sendSubagentMessage( - ctx, - parent.ID, - child.ID, - "follow up", - SendMessageBusyBehaviorInterrupt, - ) - require.NoError(t, err) - - gatewayKey, err := db.GetChatGatewayAPIKey(ctx, database.GetChatGatewayAPIKeyParams{ - UserID: user.ID, - TokenName: GatewayTokenName(user.ID), - }) - require.NoError(t, err) - messages, err := db.GetChatMessagesByChatID(ctx, database.GetChatMessagesByChatIDParams{ - ChatID: child.ID, - AfterID: 0, - }) - require.NoError(t, err) - var latestUserMessage database.ChatMessage - for _, message := range messages { - if message.Role == database.ChatMessageRoleUser && message.ID > latestUserMessage.ID { - latestUserMessage = message - } - } - require.NotZero(t, latestUserMessage.ID) - require.True(t, latestUserMessage.APIKeyID.Valid) - require.Equal(t, gatewayKey.ID, latestUserMessage.APIKeyID.String) -} - func TestCreateChildSubagentChatInheritsWorkspaceBinding(t *testing.T) { t.Parallel() diff --git a/coderd/x/chatd/synthetickey_internal_test.go b/coderd/x/chatd/synthetickey_internal_test.go index fa63cf24e4600..5c18d783aaf6d 100644 --- a/coderd/x/chatd/synthetickey_internal_test.go +++ b/coderd/x/chatd/synthetickey_internal_test.go @@ -217,18 +217,16 @@ func TestSyntheticAPIKeyDeletionDoesNotMutateChatState(t *testing.T) { OwnerID: user.ID, LastModelConfigID: model.ID, }) - message := dbgen.ChatMessage(t, db, database.ChatMessage{ + dbgen.ChatMessage(t, db, database.ChatMessage{ ChatID: chat.ID, CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: model.ID, Valid: true}, Role: database.ChatMessageRoleUser, - APIKeyID: sql.NullString{String: syntheticID, Valid: true}, }) - queued, err := db.InsertChatQueuedMessage(t.Context(), database.InsertChatQueuedMessageParams{ + _, err = db.InsertChatQueuedMessage(t.Context(), database.InsertChatQueuedMessageParams{ ChatID: chat.ID, Content: json.RawMessage(`[]`), ModelConfigID: uuid.NullUUID{UUID: model.ID, Valid: true}, - APIKeyID: sql.NullString{String: syntheticID, Valid: true}, }) require.NoError(t, err) @@ -247,16 +245,6 @@ func TestSyntheticAPIKeyDeletionDoesNotMutateChatState(t *testing.T) { require.Equal(t, before.QueueVersion, after.QueueVersion) require.Equal(t, before.GenerationAttempt, after.GenerationAttempt) - stored, err := db.GetChatMessageByID(t.Context(), message.ID) - require.NoError(t, err) - require.Equal(t, sql.NullString{String: syntheticID, Valid: true}, stored.APIKeyID) - storedQueued, err := db.GetChatQueuedMessageByID(t.Context(), database.GetChatQueuedMessageByIDParams{ - ID: queued.ID, - ChatID: chat.ID, - }) - require.NoError(t, err) - require.Equal(t, sql.NullString{String: syntheticID, Valid: true}, storedQueued.APIKeyID) - remintedID, err := server.ensureSyntheticAPIKeyID(t.Context(), user.ID) require.NoError(t, err) require.NotEqual(t, syntheticID, remintedID) diff --git a/coderd/x/chatd/tasks_test.go b/coderd/x/chatd/tasks_test.go index 31882f5991903..8f95da4e1933d 100644 --- a/coderd/x/chatd/tasks_test.go +++ b/coderd/x/chatd/tasks_test.go @@ -1044,7 +1044,6 @@ func taskUserTextMessage(t *testing.T, text string, createdBy uuid.UUID, modelCo ContentVersion: chatprompt.CurrentContentVersion, CreatedBy: uuid.NullUUID{UUID: createdBy, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true}, - APIKeyID: sql.NullString{String: apiKeyID, Valid: apiKeyID != ""}, } } diff --git a/coderd/x/chatd/turn_summary_internal_test.go b/coderd/x/chatd/turn_summary_internal_test.go index 06f5fc3f69ead..e37f79231e27d 100644 --- a/coderd/x/chatd/turn_summary_internal_test.go +++ b/coderd/x/chatd/turn_summary_internal_test.go @@ -57,7 +57,6 @@ func TestUpdateLastTurnSummaryRejectsStaleWrites(t *testing.T) { codersdk.ChatMessageText("hello"), }) require.NoError(t, err) - apiKey, _ := dbgen.APIKey(t, db, database.APIKey{UserID: owner.ID}) created, err := chatstate.CreateChat(ctx, db, ps, chatstate.CreateChatInput{ OrganizationID: org.ID, OwnerID: owner.ID, @@ -72,7 +71,6 @@ func TestUpdateLastTurnSummaryRejectsStaleWrites(t *testing.T) { ContentVersion: chatprompt.CurrentContentVersion, CreatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true}, ModelConfigID: uuid.NullUUID{UUID: modelCfg.ID, Valid: true}, - APIKeyID: sql.NullString{String: apiKey.ID, Valid: true}, }, }, }) From c4171a09aecc4f98bc2a5271a73a071f7020e726 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:23:38 +0000 Subject: [PATCH 2/4] fix(coderd/x/chatd): validate model config before chat create work --- coderd/x/chatd/chatd.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go index 22ba81e1f3b0c..13e5a2f1c966c 100644 --- a/coderd/x/chatd/chatd.go +++ b/coderd/x/chatd/chatd.go @@ -1286,6 +1286,12 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C return database.Chat{}, limitErr } + if opts.ModelConfigID != uuid.Nil { + if err := requireEnabledChatModelConfig(ctx, p.db, opts.ModelConfigID); err != nil { + return database.Chat{}, err + } + } + labelsJSON, err := json.Marshal(opts.Labels) if err != nil { return database.Chat{}, xerrors.Errorf("marshal labels: %w", err) @@ -1329,12 +1335,6 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C initialMessages = append(initialMessages, systemMessage(workspaceAwarenessContent, opts.ModelConfigID)) initialMessages = append(initialMessages, userMessage(userContent, opts.ModelConfigID, opts.OwnerID, opts.ReasoningEffort)) - if opts.ModelConfigID != uuid.Nil { - if err := requireEnabledChatModelConfig(ctx, p.db, opts.ModelConfigID); err != nil { - return database.Chat{}, err - } - } - result, err := chatstate.CreateChat(ctx, p.db, p.pubsub, chatstate.CreateChatInput{ OrganizationID: opts.OrganizationID, OwnerID: opts.OwnerID, From bab6732c50cf05996cf04aa7471b58a517fa7bfe Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:45:15 +0000 Subject: [PATCH 3/4] refactor(coderd): drop no-op aliases and refactor leftovers from key removal --- coderd/exp_chats.go | 2 +- coderd/x/chatd/chatd.go | 8 ++------ coderd/x/chatd/chatstate/transitions.go | 25 ++++++++----------------- coderd/x/chatd/stream_loop.go | 3 +-- 4 files changed, 12 insertions(+), 26 deletions(-) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 7bf3880955e6a..6ae28da6a91d1 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -6867,7 +6867,7 @@ func convertChatQueuedMessagePtr(m database.ChatQueuedMessage) *codersdk.ChatQue func convertChatQueuedMessages(msgs []database.ChatQueuedMessage) []codersdk.ChatQueuedMessage { result := make([]codersdk.ChatQueuedMessage, 0, len(msgs)) for _, m := range msgs { - result = append(result, db2sdk.ChatQueuedMessage(m)) + result = append(result, convertChatQueuedMessage(m)) } return result } diff --git a/coderd/x/chatd/chatd.go b/coderd/x/chatd/chatd.go index 13e5a2f1c966c..39a76d1a79cd9 100644 --- a/coderd/x/chatd/chatd.go +++ b/coderd/x/chatd/chatd.go @@ -2779,10 +2779,6 @@ func appendMessageFields( params.RuntimeMs = append(params.RuntimeMs, msg.runtimeMs) } -func appendChatMessage(params *database.InsertChatMessagesParams, msg chatMessage) { - appendMessageFields(params, msg) -} - // BuildSingleChatMessageInsertParams builds insert parameters for one chat message. func BuildSingleChatMessageInsertParams( chatID uuid.UUID, @@ -2793,14 +2789,14 @@ func BuildSingleChatMessageInsertParams( contentVersion int16, createdBy uuid.UUID, ) database.InsertChatMessagesParams { - params := database.InsertChatMessagesParams{ //nolint:exhaustruct // Fields populated by appendChatMessage. + params := database.InsertChatMessagesParams{ //nolint:exhaustruct // Fields populated by appendMessageFields. ChatID: chatID, } msg := newChatMessage(role, content, visibility, modelConfigID, contentVersion) if createdBy != uuid.Nil { msg = msg.withCreatedBy(createdBy) } - appendChatMessage(¶ms, msg) + appendMessageFields(¶ms, msg) return params } diff --git a/coderd/x/chatd/chatstate/transitions.go b/coderd/x/chatd/chatstate/transitions.go index 86f57e1f43464..7b4eb1daf1f0c 100644 --- a/coderd/x/chatd/chatstate/transitions.go +++ b/coderd/x/chatd/chatstate/transitions.go @@ -230,17 +230,13 @@ func (tx *Tx) insertQueuedMessage(ownerFallback uuid.UUID, m Message) (database. if err := tx.requireQueueCapacity(); err != nil { return database.ChatQueuedMessage{}, err } - row, err := tx.store.InsertChatQueuedMessageWithCreator(tx.ctx, database.InsertChatQueuedMessageWithCreatorParams{ + return tx.store.InsertChatQueuedMessageWithCreator(tx.ctx, database.InsertChatQueuedMessageWithCreatorParams{ ChatID: tx.chatID, Content: rawContent, ModelConfigID: m.ModelConfigID, ReasoningEffort: m.ReasoningEffort, CreatedBy: createdBy, }) - if err != nil { - return database.ChatQueuedMessage{}, err - } - return row, nil } // messageFromQueuedRow synthesizes a Message from a stored queued row, @@ -418,12 +414,11 @@ func (tx *Tx) sendMessageE1(chat database.Chat, m Message) (SendMessageResult, e if err != nil { return SendMessageResult{}, xerrors.Errorf("get queue head: %w", err) } - headMessage := head cancels, err := synthesizePendingToolCancellations(tx.ctx, tx.store, chat, "Tool execution interrupted by queued message promotion", false) if err != nil { return SendMessageResult{}, err } - promoted := messageFromQueuedRow(headMessage) + promoted := messageFromQueuedRow(head) inserted, err := tx.insertMessages(append(cancels, promoted)) if err != nil { return SendMessageResult{}, xerrors.Errorf("insert promoted queued head: %w", err) @@ -640,7 +635,6 @@ func (tx *Tx) DeleteQueuedMessage(input DeleteQueuedMessageInput) (DeleteQueuedM if err != nil { return DeleteQueuedMessageResult{}, xerrors.Errorf("get queued: %w", err) } - targetMessage := target rows, err := tx.store.DeleteChatQueuedMessageReturningCount(tx.ctx, database.DeleteChatQueuedMessageReturningCountParams{ ID: input.QueuedMessageID, ChatID: tx.chatID, @@ -652,7 +646,7 @@ func (tx *Tx) DeleteQueuedMessage(input DeleteQueuedMessageInput) (DeleteQueuedM return DeleteQueuedMessageResult{}, ErrQueuedMessageNotFound } return DeleteQueuedMessageResult{ - DeletedQueuedMessage: targetMessage, + DeletedQueuedMessage: target, }, nil } @@ -686,7 +680,6 @@ func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueu if err != nil { return PromoteQueuedMessageResult{}, xerrors.Errorf("get queued: %w", err) } - targetMessage := target rows, err := tx.store.ReorderChatQueuedMessageToHead(tx.ctx, database.ReorderChatQueuedMessageToHeadParams{ ID: input.QueuedMessageID, ChatID: tx.chatID, @@ -712,7 +705,7 @@ func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueu return PromoteQueuedMessageResult{}, xerrors.Errorf("set interrupting: %w", err) } return PromoteQueuedMessageResult{ - QueuedMessage: targetMessage, + QueuedMessage: target, ReorderedQueueOnly: reorderOnly, }, nil } @@ -725,7 +718,7 @@ func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueu if err != nil { return PromoteQueuedMessageResult{}, err } - promotedMsg := messageFromQueuedRow(targetMessage) + promotedMsg := messageFromQueuedRow(target) inserted, err := tx.insertMessages(append(cancels, promotedMsg)) if err != nil { return PromoteQueuedMessageResult{}, xerrors.Errorf("insert promoted queued message: %w", err) @@ -755,7 +748,7 @@ func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueu cancellations := inserted[:len(inserted)-1] insertedUserMsg := inserted[len(inserted)-1] return PromoteQueuedMessageResult{ - QueuedMessage: targetMessage, + QueuedMessage: target, InsertedMessage: &insertedUserMsg, CancellationMessages: cancellations, ReorderedQueueOnly: reorderOnly, @@ -1210,8 +1203,7 @@ func (tx *Tx) FinishInterruption(input FinishInterruptionInput) (FinishInterrupt if err != nil { return FinishInterruptionResult{}, xerrors.Errorf("get queue head: %w", err) } - headMessage := head - promotedMsg := messageFromQueuedRow(headMessage) + promotedMsg := messageFromQueuedRow(head) insertedHead, err := tx.insertMessages([]Message{promotedMsg}) if err != nil { return FinishInterruptionResult{}, xerrors.Errorf("insert promoted queue head: %w", err) @@ -1277,12 +1269,11 @@ func (tx *Tx) FinishTurn(_ FinishTurnInput) (FinishTurnResult, error) { if err != nil { return FinishTurnResult{}, xerrors.Errorf("get queue head: %w", err) } - headMessage := head cancels, err := synthesizePendingToolCancellations(tx.ctx, tx.store, chat, "Tool execution interrupted by queued message promotion", false) if err != nil { return FinishTurnResult{}, err } - promotedMsg := messageFromQueuedRow(headMessage) + promotedMsg := messageFromQueuedRow(head) inserted, err := tx.insertMessages(append(cancels, promotedMsg)) if err != nil { return FinishTurnResult{}, xerrors.Errorf("insert promoted queue head: %w", err) diff --git a/coderd/x/chatd/stream_loop.go b/coderd/x/chatd/stream_loop.go index 06971c0e305bb..5004e8a490ea8 100644 --- a/coderd/x/chatd/stream_loop.go +++ b/coderd/x/chatd/stream_loop.go @@ -176,11 +176,10 @@ func (l *streamLoop) loadDBSnapshot(ctx context.Context) (streamDBSnapshot, erro if chat.QueueVersion > l.state.queueVersion { snapshot.queueChanged = true - queued, err := tx.GetChatQueuedMessages(ctx, l.chatID) + snapshot.queue, err = tx.GetChatQueuedMessages(ctx, l.chatID) if err != nil { return xerrors.Errorf("get chat queue: %w", err) } - snapshot.queue = queued } if chat.Status == database.ChatStatusRequiresAction { From bbec1dbb443045ce0d93037dbb3eb0ee198b43dd Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:09:12 +0000 Subject: [PATCH 4/4] fix(coderd/database/migrations): renumber drop migration to 000548 after main collision --- ...mns.down.sql => 000548_drop_chat_gateway_key_columns.down.sql} | 0 ...columns.up.sql => 000548_drop_chat_gateway_key_columns.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000547_drop_chat_gateway_key_columns.down.sql => 000548_drop_chat_gateway_key_columns.down.sql} (100%) rename coderd/database/migrations/{000547_drop_chat_gateway_key_columns.up.sql => 000548_drop_chat_gateway_key_columns.up.sql} (100%) diff --git a/coderd/database/migrations/000547_drop_chat_gateway_key_columns.down.sql b/coderd/database/migrations/000548_drop_chat_gateway_key_columns.down.sql similarity index 100% rename from coderd/database/migrations/000547_drop_chat_gateway_key_columns.down.sql rename to coderd/database/migrations/000548_drop_chat_gateway_key_columns.down.sql diff --git a/coderd/database/migrations/000547_drop_chat_gateway_key_columns.up.sql b/coderd/database/migrations/000548_drop_chat_gateway_key_columns.up.sql similarity index 100% rename from coderd/database/migrations/000547_drop_chat_gateway_key_columns.up.sql rename to coderd/database/migrations/000548_drop_chat_gateway_key_columns.up.sql