mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
Generated
+14
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user