diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2ba4b923de..1a43f8ddb9 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -2125,7 +2125,20 @@ LEFT JOIN LATERAL ( SELECT (ARRAY_AGG(ai.client ORDER BY ai.started_at, ai.id))[1] AS client, (ARRAY_AGG(ai.metadata ORDER BY ai.started_at, ai.id))[1] AS metadata, - ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers, + -- Order providers by usage frequency (most-used first) so the + -- "primary" chat provider appears at index 0 even when a + -- secondary provider (e.g. Anthropic Haiku for title generation) + -- is also present in the session. + (SELECT array_agg(sub.provider ORDER BY sub.cnt DESC, sub.provider) + FROM ( + SELECT ai2.provider, COUNT(*) AS cnt + FROM aibridge_interceptions ai2 + WHERE ai2.session_id = sp.session_id + AND ai2.initiator_id = sp.initiator_id + AND ai2.ended_at IS NOT NULL + GROUP BY ai2.provider + ) sub + ) AS providers, ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models, ARRAY_AGG(ai.id) AS interception_ids FROM aibridge_interceptions ai diff --git a/coderd/database/queries/aibridge.sql b/coderd/database/queries/aibridge.sql index 7756c7086b..11147a5eb4 100644 --- a/coderd/database/queries/aibridge.sql +++ b/coderd/database/queries/aibridge.sql @@ -591,7 +591,20 @@ LEFT JOIN LATERAL ( SELECT (ARRAY_AGG(ai.client ORDER BY ai.started_at, ai.id))[1] AS client, (ARRAY_AGG(ai.metadata ORDER BY ai.started_at, ai.id))[1] AS metadata, - ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers, + -- Order providers by usage frequency (most-used first) so the + -- "primary" chat provider appears at index 0 even when a + -- secondary provider (e.g. Anthropic Haiku for title generation) + -- is also present in the session. + (SELECT array_agg(sub.provider ORDER BY sub.cnt DESC, sub.provider) + FROM ( + SELECT ai2.provider, COUNT(*) AS cnt + FROM aibridge_interceptions ai2 + WHERE ai2.session_id = sp.session_id + AND ai2.initiator_id = sp.initiator_id + AND ai2.ended_at IS NOT NULL + GROUP BY ai2.provider + ) sub + ) AS providers, ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models, ARRAY_AGG(ai.id) AS interception_ids FROM aibridge_interceptions ai diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index 158f682842..9c46b845bb 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -840,6 +840,49 @@ func TestAIBridgeListSessions(t *testing.T) { require.ElementsMatch(t, []string{"claude-4", "gpt-4"}, s4.Models) }) + // Regression test for https://linear.app/codercom/issue/AIGOV-403 + t.Run("ProviderOrderByFrequency", func(t *testing.T) { + t.Parallel() + client, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t)) + ctx := testutil.Context(t, testutil.WaitLong) + + now := dbtime.Now() + + // Simulate an OpenAI chat session with 3 OpenAI interceptions + // and 1 Anthropic interception (title generation via Haiku). + for i := range 3 { + endedAt := now.Add(time.Duration(i+1) * time.Minute) + dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ + InitiatorID: firstUser.UserID, + Provider: "openai", + Model: "gpt-4o", + StartedAt: now.Add(time.Duration(i) * time.Minute), + Client: sql.NullString{String: "cursor", Valid: true}, + ClientSessionID: sql.NullString{String: "session-freq", Valid: true}, + }, &endedAt) + } + // Single Anthropic title-generation interception in the same session. + haikuEnded := now.Add(30 * time.Second) + dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{ + InitiatorID: firstUser.UserID, + Provider: "anthropic", + Model: "claude-haiku-4-5", + StartedAt: now.Add(10 * time.Second), + Client: sql.NullString{String: "cursor", Valid: true}, + ClientSessionID: sql.NullString{String: "session-freq", Valid: true}, + }, &haikuEnded) + + //nolint:gocritic // Owner role is irrelevant here. + res, err := client.AIBridgeListSessions(ctx, codersdk.AIBridgeListSessionsFilter{}) + require.NoError(t, err) + require.Len(t, res.Sessions, 1) + + s := res.Sessions[0] + require.Equal(t, "session-freq", s.ID) + // OpenAI must be first because it has more interceptions. + require.Equal(t, []string{"openai", "anthropic"}, s.Providers) + }) + t.Run("Pagination", func(t *testing.T) { t.Parallel() client, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t))