fix: order session providers by usage frequency instead of alphabetically

The AI Sessions page Provider column displayed the wrong provider when a
session contained interceptions from multiple providers. This happened
because providers were aggregated with ARRAY_AGG(DISTINCT ... ORDER BY
provider), which sorts alphabetically. Since 'anthropic' < 'openai', a
single Anthropic Haiku title-generation interception would push
Anthropic to index 0, even when the session was primarily an OpenAI
chat.

Replace the alphabetical ARRAY_AGG with a subquery that groups providers
by count and orders them most-frequent-first (with alphabetical as
tiebreaker). The frontend already displays providers[0], so the primary
chat provider now appears correctly.

Fixes: AIGOV-403
This commit is contained in:
Marcin Tojek
2026-05-28 11:51:39 +00:00
parent daf73b7b89
commit 2b488cd5ae
3 changed files with 71 additions and 2 deletions
+14 -1
View File
@@ -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
+14 -1
View File
@@ -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
+43
View File
@@ -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))