feat(coderd): include chat model organization IDs in telemetry (#27956)

Include the owning organization ID for each chat model configuration in
telemetry snapshots.

Depends on #27955

_This pull request description was generated by Coder Agents._
This commit is contained in:
Ethan
2026-08-24 09:43:44 +00:00
committed by GitHub
parent f32592ec6b
commit ac41b5b4f9
5 changed files with 41 additions and 33 deletions
-1
View File
@@ -483,7 +483,6 @@ type sqlcQuerier interface {
GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatMessage, error)
GetChatModelConfigByID(ctx context.Context, id uuid.UUID) (ChatModelConfig, error)
GetChatModelConfigs(ctx context.Context) ([]ChatModelConfig, error)
// Returns all model configurations for telemetry snapshot collection.
// deleted = false guarantees ai_provider_id is non-null, so INNER JOIN is safe.
GetChatModelConfigsForTelemetry(ctx context.Context) ([]GetChatModelConfigsForTelemetryRow, error)
// GetChatPersonalModelOverridesEnabled returns whether users may configure
+12 -9
View File
@@ -8651,22 +8651,22 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI
}
const getChatModelConfigsForTelemetry = `-- name: GetChatModelConfigsForTelemetry :many
SELECT cmc.id, ap.type::text AS provider, cmc.model, cmc.context_limit, cmc.enabled, cmc.is_default
SELECT cmc.id, ap.type::text AS provider, cmc.model, cmc.context_limit, cmc.enabled, cmc.is_default, cmc.organization_id
FROM chat_model_configs cmc
JOIN ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE cmc.deleted = false
`
type GetChatModelConfigsForTelemetryRow struct {
ID uuid.UUID `db:"id" json:"id"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
ContextLimit int64 `db:"context_limit" json:"context_limit"`
Enabled bool `db:"enabled" json:"enabled"`
IsDefault bool `db:"is_default" json:"is_default"`
ID uuid.UUID `db:"id" json:"id"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
ContextLimit int64 `db:"context_limit" json:"context_limit"`
Enabled bool `db:"enabled" json:"enabled"`
IsDefault bool `db:"is_default" json:"is_default"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
// Returns all model configurations for telemetry snapshot collection.
// deleted = false guarantees ai_provider_id is non-null, so INNER JOIN is safe.
func (q *sqlQuerier) GetChatModelConfigsForTelemetry(ctx context.Context) ([]GetChatModelConfigsForTelemetryRow, error) {
rows, err := q.db.QueryContext(ctx, getChatModelConfigsForTelemetry)
@@ -8684,6 +8684,7 @@ func (q *sqlQuerier) GetChatModelConfigsForTelemetry(ctx context.Context) ([]Get
&i.ContextLimit,
&i.Enabled,
&i.IsDefault,
&i.OrganizationID,
); err != nil {
return nil, err
}
@@ -9672,7 +9673,7 @@ func (q *sqlQuerier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID
const getChatsUpdatedAfter = `-- name: GetChatsUpdatedAfter :many
SELECT
c.id, c.owner_id, c.created_at, c.updated_at, c.status,
c.id, c.owner_id, c.organization_id, c.created_at, c.updated_at, c.status,
(c.parent_chat_id IS NOT NULL)::bool AS has_parent,
c.root_chat_id, c.workspace_id,
c.mode, c.archived, c.last_model_config_id, c.client_type,
@@ -9685,6 +9686,7 @@ WHERE c.updated_at > $1
type GetChatsUpdatedAfterRow struct {
ID uuid.UUID `db:"id" json:"id"`
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Status ChatStatus `db:"status" json:"status"`
@@ -9713,6 +9715,7 @@ func (q *sqlQuerier) GetChatsUpdatedAfter(ctx context.Context, updatedAfter time
if err := rows.Scan(
&i.ID,
&i.OwnerID,
&i.OrganizationID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
+2 -3
View File
@@ -2246,7 +2246,7 @@ WHERE chats.id = deletable.id
-- snapshot collection. Uses updated_at so that long-running chats
-- still appear in each snapshot window while they are active.
SELECT
c.id, c.owner_id, c.created_at, c.updated_at, c.status,
c.id, c.owner_id, c.organization_id, c.created_at, c.updated_at, c.status,
(c.parent_chat_id IS NOT NULL)::bool AS has_parent,
c.root_chat_id, c.workspace_id,
c.mode, c.archived, c.last_model_config_id, c.client_type,
@@ -2280,9 +2280,8 @@ WHERE cm.created_at > @created_after
GROUP BY cm.chat_id;
-- name: GetChatModelConfigsForTelemetry :many
-- Returns all model configurations for telemetry snapshot collection.
-- deleted = false guarantees ai_provider_id is non-null, so INNER JOIN is safe.
SELECT cmc.id, ap.type::text AS provider, cmc.model, cmc.context_limit, cmc.enabled, cmc.is_default
SELECT cmc.id, ap.type::text AS provider, cmc.model, cmc.context_limit, cmc.enabled, cmc.is_default, cmc.organization_id
FROM chat_model_configs cmc
JOIN ai_providers ap ON ap.id = cmc.ai_provider_id
WHERE cmc.deleted = false;
+17 -12
View File
@@ -2270,6 +2270,7 @@ func ConvertChat(dbChat database.GetChatsUpdatedAfterRow) Chat {
c := Chat{
ID: dbChat.ID,
OwnerID: dbChat.OwnerID,
OrganizationID: dbChat.OrganizationID,
CreatedAt: dbChat.CreatedAt,
UpdatedAt: dbChat.UpdatedAt,
Status: string(dbChat.Status),
@@ -2319,12 +2320,13 @@ func ConvertChatMessageSummary(dbRow database.GetChatMessageSummariesPerChatRow)
// telemetry ChatModelConfig.
func ConvertChatModelConfig(dbRow database.GetChatModelConfigsForTelemetryRow) ChatModelConfig {
return ChatModelConfig{
ID: dbRow.ID,
Provider: dbRow.Provider,
Model: dbRow.Model,
ContextLimit: dbRow.ContextLimit,
Enabled: dbRow.Enabled,
IsDefault: dbRow.IsDefault,
ID: dbRow.ID,
OrganizationID: dbRow.OrganizationID,
Provider: dbRow.Provider,
Model: dbRow.Model,
ContextLimit: dbRow.ContextLimit,
Enabled: dbRow.Enabled,
IsDefault: dbRow.IsDefault,
}
}
@@ -2577,6 +2579,7 @@ type BoundaryUsageSummary struct {
type Chat struct {
ID uuid.UUID `json:"id"`
OwnerID uuid.UUID `json:"owner_id"`
OrganizationID uuid.UUID `json:"organization_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Status string `json:"status"`
@@ -2612,12 +2615,14 @@ type ChatMessageSummary struct {
// ChatModelConfig contains model configuration metadata for
// telemetry. Sensitive fields like API keys are excluded.
type ChatModelConfig struct {
ID uuid.UUID `json:"id"`
Provider string `json:"provider"`
Model string `json:"model"`
ContextLimit int64 `json:"context_limit"`
Enabled bool `json:"enabled"`
IsDefault bool `json:"is_default"`
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
Provider string `json:"provider"`
Model string `json:"model"`
ContextLimit int64 `json:"context_limit"`
Enabled bool `json:"enabled"`
// Each organization has at most one default configuration.
IsDefault bool `json:"is_default"`
}
// ChatDiffStatusSummary contains aggregate PR counts across all
+10 -8
View File
@@ -1648,14 +1648,15 @@ func TestChatsTelemetry(t *testing.T) {
ContextLimit: 200000,
})
// Create a second model config to test full dump.
org2 := dbgen.Organization(t, db, database.Organization{})
modelCfg2 := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
AIProviderID: uuid.NullUUID{UUID: openaiProvider.ID, Valid: true},
Model: "gpt-4o",
DisplayName: "GPT-4o",
AIProviderID: uuid.NullUUID{UUID: openaiProvider.ID, Valid: true},
OrganizationID: org2.ID,
Model: "gpt-4o",
DisplayName: "GPT-4o",
})
// Create a soft-deleted model config — should NOT appear in telemetry.
// Soft-deleted model configurations must not appear in telemetry.
deletedCfg := dbgen.ChatModelConfig(t, db, database.ChatModelConfig{
AIProviderID: uuid.NullUUID{UUID: anthropicProvider.ID, Valid: true},
Model: "claude-deleted",
@@ -1814,9 +1815,7 @@ func TestChatsTelemetry(t *testing.T) {
ProviderResponseID: sql.NullString{String: "resp-3", Valid: true},
})
// Insert a soft-deleted message on root chat with large token values.
// This acts as "poison" — if the deleted filter is missing, totals
// will be inflated and assertions below will fail.
// Large token values expose a missing soft-delete filter in the totals.
poisonMsg := dbgen.ChatMessage(t, db, database.ChatMessage{
ChatID: rootChat.ID,
ModelConfigID: uuid.NullUUID{UUID: modelCfg.ID, Valid: true},
@@ -1854,6 +1853,7 @@ func TestChatsTelemetry(t *testing.T) {
// Root chat assertions.
assert.Equal(t, rootChat.ID, foundRoot.ID)
assert.Equal(t, user.ID, foundRoot.OwnerID)
assert.Equal(t, org.ID, foundRoot.OrganizationID)
assert.Equal(t, "running", foundRoot.Status)
assert.False(t, foundRoot.HasParent)
assert.Nil(t, foundRoot.RootChatID)
@@ -1935,6 +1935,7 @@ func TestChatsTelemetry(t *testing.T) {
cfg1, ok := configMap[modelCfg.ID]
require.True(t, ok)
assert.Equal(t, org.ID, cfg1.OrganizationID)
assert.Equal(t, "anthropic", cfg1.Provider)
assert.Equal(t, "claude-sonnet-4-20250514", cfg1.Model)
assert.Equal(t, int64(200000), cfg1.ContextLimit)
@@ -1943,6 +1944,7 @@ func TestChatsTelemetry(t *testing.T) {
cfg2, ok := configMap[modelCfg2.ID]
require.True(t, ok)
assert.Equal(t, org2.ID, cfg2.OrganizationID)
assert.Equal(t, "openai", cfg2.Provider)
assert.Equal(t, "gpt-4o", cfg2.Model)
assert.Equal(t, int64(128000), cfg2.ContextLimit)