mirror of
https://github.com/coder/coder.git
synced 2026-10-31 19:36:41 +08:00
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:
Generated
-1
@@ -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
|
||||
|
||||
Generated
+12
-9
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user