mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor!: remove interceptions API, request logs view, and associated code (#26213)
## Summary Removes the deprecated `/api/v2/aibridge/interceptions` endpoint and the Request Logs frontend page, both replaced by the session-based view. Closes https://linear.app/codercom/issue/AIGOV-266 Closes https://linear.app/codercom/issue/AIGOV-324 ## Changes ### Backend - Remove `GET /api/v2/aibridge/interceptions` HTTP handler and route - Remove SDK types and client method (`AIBridgeInterception`, `AIBridgeTokenUsage`, `AIBridgeUserPrompt`, `AIBridgeToolUsage`, `AIBridgeListInterceptionsResponse`, `AIBridgeListInterceptionsFilter`) - Remove SQL queries `CountAIBridgeInterceptions` and `ListAIBridgeInterceptions` - Remove `searchquery.AIBridgeInterceptions` parser - Remove dbauthz wrappers, in-memory implementations, metrics, and mocks for the interceptions list queries - Remove the `coder aibridge interceptions list` CLI command and golden files - Regenerate API docs, swagger, mocks, and metrics The `/models`, `/clients`, and `/sessions` endpoints stay; the sessions list page still consumes all three. ### Frontend - Delete the entire `RequestLogsPage/` directory (page, view, row, filter, stories, tests) - Remove the `/aibridge/request-logs` route and its lazy import - Remove the `getAIBridgeInterceptions` API method, `paginatedInterceptions` query, and mock interception entities - `git mv` the shared filter and icon components used by the sessions pages: - `RequestLogsPage/RequestLogsFilter/{Client,Model,Provider}Filter.tsx` → `AIBridgePage/filters/` - `RequestLogsPage/icons/AIBridge{Client,Model,Provider}Icon.tsx` → `AIBridgePage/icons/` - Drop the `getProviderIconName` hack and the duplicate `anthropic-neue` icon case now that the FIXME no longer applies ## Commits 1. `refactor: remove interceptions API and request logs view` — the bulk removal, with explicit renames for the shared filter/icon files. 2. `refactor(site/src/pages/AIBridgePage): drop getProviderIconName hack` — cleanup of the FIXME that depended on RequestLogsPage existing. > [!NOTE] > Generated by Coder Agents on behalf of @dannykopping
This commit is contained in:
@@ -1093,46 +1093,6 @@ func PreviewParameterValidation(v *previewtypes.ParameterValidation) codersdk.Pr
|
||||
}
|
||||
}
|
||||
|
||||
func AIBridgeInterception(interception database.AIBridgeInterception, initiator database.VisibleUser, tokenUsages []database.AIBridgeTokenUsage, userPrompts []database.AIBridgeUserPrompt, toolUsages []database.AIBridgeToolUsage) codersdk.AIBridgeInterception {
|
||||
sdkTokenUsages := slice.List(tokenUsages, AIBridgeTokenUsage)
|
||||
sort.Slice(sdkTokenUsages, func(i, j int) bool {
|
||||
// created_at ASC
|
||||
return sdkTokenUsages[i].CreatedAt.Before(sdkTokenUsages[j].CreatedAt)
|
||||
})
|
||||
sdkUserPrompts := slice.List(userPrompts, AIBridgeUserPrompt)
|
||||
sort.Slice(sdkUserPrompts, func(i, j int) bool {
|
||||
// created_at ASC
|
||||
return sdkUserPrompts[i].CreatedAt.Before(sdkUserPrompts[j].CreatedAt)
|
||||
})
|
||||
sdkToolUsages := slice.List(toolUsages, AIBridgeToolUsage)
|
||||
sort.Slice(sdkToolUsages, func(i, j int) bool {
|
||||
// created_at ASC
|
||||
return sdkToolUsages[i].CreatedAt.Before(sdkToolUsages[j].CreatedAt)
|
||||
})
|
||||
intc := codersdk.AIBridgeInterception{
|
||||
ID: interception.ID,
|
||||
Initiator: MinimalUserFromVisibleUser(initiator),
|
||||
Provider: interception.Provider,
|
||||
ProviderName: interception.ProviderName,
|
||||
Model: interception.Model,
|
||||
Metadata: jsonOrEmptyMap(interception.Metadata),
|
||||
StartedAt: interception.StartedAt,
|
||||
TokenUsages: sdkTokenUsages,
|
||||
UserPrompts: sdkUserPrompts,
|
||||
ToolUsages: sdkToolUsages,
|
||||
}
|
||||
if interception.APIKeyID.Valid {
|
||||
intc.APIKeyID = &interception.APIKeyID.String
|
||||
}
|
||||
if interception.EndedAt.Valid {
|
||||
intc.EndedAt = &interception.EndedAt.Time
|
||||
}
|
||||
if interception.Client.Valid {
|
||||
intc.Client = &interception.Client.String
|
||||
}
|
||||
return intc
|
||||
}
|
||||
|
||||
func AIBridgeSession(row database.ListAIBridgeSessionsRow) codersdk.AIBridgeSession {
|
||||
session := codersdk.AIBridgeSession{
|
||||
ID: row.SessionID,
|
||||
@@ -1174,46 +1134,6 @@ func AIBridgeSession(row database.ListAIBridgeSessionsRow) codersdk.AIBridgeSess
|
||||
return session
|
||||
}
|
||||
|
||||
func AIBridgeTokenUsage(usage database.AIBridgeTokenUsage) codersdk.AIBridgeTokenUsage {
|
||||
return codersdk.AIBridgeTokenUsage{
|
||||
ID: usage.ID,
|
||||
InterceptionID: usage.InterceptionID,
|
||||
ProviderResponseID: usage.ProviderResponseID,
|
||||
InputTokens: usage.InputTokens,
|
||||
OutputTokens: usage.OutputTokens,
|
||||
CacheReadInputTokens: usage.CacheReadInputTokens,
|
||||
CacheWriteInputTokens: usage.CacheWriteInputTokens,
|
||||
Metadata: jsonOrEmptyMap(usage.Metadata),
|
||||
CreatedAt: usage.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func AIBridgeUserPrompt(prompt database.AIBridgeUserPrompt) codersdk.AIBridgeUserPrompt {
|
||||
return codersdk.AIBridgeUserPrompt{
|
||||
ID: prompt.ID,
|
||||
InterceptionID: prompt.InterceptionID,
|
||||
ProviderResponseID: prompt.ProviderResponseID,
|
||||
Prompt: prompt.Prompt,
|
||||
Metadata: jsonOrEmptyMap(prompt.Metadata),
|
||||
CreatedAt: prompt.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func AIBridgeToolUsage(usage database.AIBridgeToolUsage) codersdk.AIBridgeToolUsage {
|
||||
return codersdk.AIBridgeToolUsage{
|
||||
ID: usage.ID,
|
||||
InterceptionID: usage.InterceptionID,
|
||||
ProviderResponseID: usage.ProviderResponseID,
|
||||
ServerURL: usage.ServerUrl.String,
|
||||
Tool: usage.Tool,
|
||||
Input: usage.Input,
|
||||
Injected: usage.Injected,
|
||||
InvocationError: usage.InvocationError.String,
|
||||
Metadata: jsonOrEmptyMap(usage.Metadata),
|
||||
CreatedAt: usage.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// AIBridgeSessionThreads converts session metadata and thread interceptions
|
||||
// into the threads response. It groups interceptions into threads, builds
|
||||
// agentic actions from tool usages and model thoughts, and aggregates
|
||||
|
||||
@@ -599,238 +599,6 @@ func TestChatDebugRunDetail_NullableFieldsNil(t *testing.T) {
|
||||
require.Empty(t, sdk.Steps)
|
||||
}
|
||||
|
||||
func TestAIBridgeInterception(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := dbtime.Now()
|
||||
interceptionID := uuid.New()
|
||||
initiatorID := uuid.New()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
interception database.AIBridgeInterception
|
||||
initiator database.VisibleUser
|
||||
tokenUsages []database.AIBridgeTokenUsage
|
||||
userPrompts []database.AIBridgeUserPrompt
|
||||
toolUsages []database.AIBridgeToolUsage
|
||||
expected codersdk.AIBridgeInterception
|
||||
}{
|
||||
{
|
||||
name: "all_optional_values_set",
|
||||
interception: database.AIBridgeInterception{
|
||||
ID: interceptionID,
|
||||
InitiatorID: initiatorID,
|
||||
Provider: "anthropic",
|
||||
Model: "claude-3-opus",
|
||||
StartedAt: now,
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"key":"value"}`),
|
||||
Valid: true,
|
||||
},
|
||||
EndedAt: sql.NullTime{
|
||||
Time: now.Add(time.Minute),
|
||||
Valid: true,
|
||||
},
|
||||
APIKeyID: sql.NullString{
|
||||
String: "api-key-123",
|
||||
Valid: true,
|
||||
},
|
||||
Client: sql.NullString{
|
||||
String: "claude-code/1.0.0",
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
initiator: database.VisibleUser{
|
||||
ID: initiatorID,
|
||||
Username: "testuser",
|
||||
Name: "Test User",
|
||||
AvatarURL: "https://example.com/avatar.png",
|
||||
},
|
||||
tokenUsages: []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
InterceptionID: interceptionID,
|
||||
ProviderResponseID: "resp-123",
|
||||
InputTokens: 100,
|
||||
OutputTokens: 200,
|
||||
CacheReadInputTokens: 50,
|
||||
CacheWriteInputTokens: 10,
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"cache":"hit"}`),
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: now.Add(10 * time.Second),
|
||||
},
|
||||
},
|
||||
userPrompts: []database.AIBridgeUserPrompt{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
InterceptionID: interceptionID,
|
||||
ProviderResponseID: "resp-123",
|
||||
Prompt: "Hello, world!",
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"role":"user"}`),
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: now.Add(5 * time.Second),
|
||||
},
|
||||
},
|
||||
toolUsages: []database.AIBridgeToolUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
InterceptionID: interceptionID,
|
||||
ProviderResponseID: "resp-123",
|
||||
ServerUrl: sql.NullString{
|
||||
String: "https://mcp.example.com",
|
||||
Valid: true,
|
||||
},
|
||||
Tool: "read_file",
|
||||
Input: `{"path":"/tmp/test.txt"}`,
|
||||
Injected: true,
|
||||
InvocationError: sql.NullString{
|
||||
String: "file not found",
|
||||
Valid: true,
|
||||
},
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"duration_ms":50}`),
|
||||
Valid: true,
|
||||
},
|
||||
CreatedAt: now.Add(15 * time.Second),
|
||||
},
|
||||
},
|
||||
expected: codersdk.AIBridgeInterception{
|
||||
ID: interceptionID,
|
||||
Initiator: codersdk.MinimalUser{
|
||||
ID: initiatorID,
|
||||
Username: "testuser",
|
||||
Name: "Test User",
|
||||
AvatarURL: "https://example.com/avatar.png",
|
||||
},
|
||||
Provider: "anthropic",
|
||||
Model: "claude-3-opus",
|
||||
Metadata: map[string]any{"key": "value"},
|
||||
StartedAt: now,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no_optional_values_set",
|
||||
interception: database.AIBridgeInterception{
|
||||
ID: interceptionID,
|
||||
InitiatorID: initiatorID,
|
||||
Provider: "openai",
|
||||
Model: "gpt-4",
|
||||
StartedAt: now,
|
||||
Metadata: pqtype.NullRawMessage{Valid: false},
|
||||
EndedAt: sql.NullTime{Valid: false},
|
||||
APIKeyID: sql.NullString{Valid: false},
|
||||
Client: sql.NullString{Valid: false},
|
||||
},
|
||||
initiator: database.VisibleUser{
|
||||
ID: initiatorID,
|
||||
Username: "minimaluser",
|
||||
Name: "",
|
||||
AvatarURL: "",
|
||||
},
|
||||
tokenUsages: nil,
|
||||
userPrompts: nil,
|
||||
toolUsages: nil,
|
||||
expected: codersdk.AIBridgeInterception{
|
||||
ID: interceptionID,
|
||||
Initiator: codersdk.MinimalUser{
|
||||
ID: initiatorID,
|
||||
Username: "minimaluser",
|
||||
Name: "",
|
||||
AvatarURL: "",
|
||||
},
|
||||
Provider: "openai",
|
||||
Model: "gpt-4",
|
||||
Metadata: nil,
|
||||
StartedAt: now,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := db2sdk.AIBridgeInterception(
|
||||
tc.interception,
|
||||
tc.initiator,
|
||||
tc.tokenUsages,
|
||||
tc.userPrompts,
|
||||
tc.toolUsages,
|
||||
)
|
||||
|
||||
// Check basic fields.
|
||||
require.Equal(t, tc.expected.ID, result.ID)
|
||||
require.Equal(t, tc.expected.Initiator, result.Initiator)
|
||||
require.Equal(t, tc.expected.Provider, result.Provider)
|
||||
require.Equal(t, tc.expected.Model, result.Model)
|
||||
require.Equal(t, tc.expected.StartedAt.UTC(), result.StartedAt.UTC())
|
||||
require.Equal(t, tc.expected.Metadata, result.Metadata)
|
||||
|
||||
// Check optional pointer fields.
|
||||
if tc.interception.APIKeyID.Valid {
|
||||
require.NotNil(t, result.APIKeyID)
|
||||
require.Equal(t, tc.interception.APIKeyID.String, *result.APIKeyID)
|
||||
} else {
|
||||
require.Nil(t, result.APIKeyID)
|
||||
}
|
||||
|
||||
if tc.interception.EndedAt.Valid {
|
||||
require.NotNil(t, result.EndedAt)
|
||||
require.Equal(t, tc.interception.EndedAt.Time.UTC(), result.EndedAt.UTC())
|
||||
} else {
|
||||
require.Nil(t, result.EndedAt)
|
||||
}
|
||||
|
||||
if tc.interception.Client.Valid {
|
||||
require.NotNil(t, result.Client)
|
||||
require.Equal(t, tc.interception.Client.String, *result.Client)
|
||||
} else {
|
||||
require.Nil(t, result.Client)
|
||||
}
|
||||
|
||||
// Check slices.
|
||||
require.Len(t, result.TokenUsages, len(tc.tokenUsages))
|
||||
require.Len(t, result.UserPrompts, len(tc.userPrompts))
|
||||
require.Len(t, result.ToolUsages, len(tc.toolUsages))
|
||||
|
||||
// Verify token usages are converted correctly.
|
||||
for i, tu := range tc.tokenUsages {
|
||||
require.Equal(t, tu.ID, result.TokenUsages[i].ID)
|
||||
require.Equal(t, tu.InterceptionID, result.TokenUsages[i].InterceptionID)
|
||||
require.Equal(t, tu.ProviderResponseID, result.TokenUsages[i].ProviderResponseID)
|
||||
require.Equal(t, tu.InputTokens, result.TokenUsages[i].InputTokens)
|
||||
require.Equal(t, tu.OutputTokens, result.TokenUsages[i].OutputTokens)
|
||||
require.Equal(t, tu.CacheReadInputTokens, result.TokenUsages[i].CacheReadInputTokens)
|
||||
require.Equal(t, tu.CacheWriteInputTokens, result.TokenUsages[i].CacheWriteInputTokens)
|
||||
}
|
||||
|
||||
// Verify user prompts are converted correctly.
|
||||
for i, up := range tc.userPrompts {
|
||||
require.Equal(t, up.ID, result.UserPrompts[i].ID)
|
||||
require.Equal(t, up.InterceptionID, result.UserPrompts[i].InterceptionID)
|
||||
require.Equal(t, up.ProviderResponseID, result.UserPrompts[i].ProviderResponseID)
|
||||
require.Equal(t, up.Prompt, result.UserPrompts[i].Prompt)
|
||||
}
|
||||
|
||||
// Verify tool usages are converted correctly.
|
||||
for i, toolUsage := range tc.toolUsages {
|
||||
require.Equal(t, toolUsage.ID, result.ToolUsages[i].ID)
|
||||
require.Equal(t, toolUsage.InterceptionID, result.ToolUsages[i].InterceptionID)
|
||||
require.Equal(t, toolUsage.ProviderResponseID, result.ToolUsages[i].ProviderResponseID)
|
||||
require.Equal(t, toolUsage.ServerUrl.String, result.ToolUsages[i].ServerURL)
|
||||
require.Equal(t, toolUsage.Tool, result.ToolUsages[i].Tool)
|
||||
require.Equal(t, toolUsage.Input, result.ToolUsages[i].Input)
|
||||
require.Equal(t, toolUsage.Injected, result.ToolUsages[i].Injected)
|
||||
require.Equal(t, toolUsage.InvocationError.String, result.ToolUsages[i].InvocationError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessage_PreservesProviderExecutedOnToolResults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user