feat: add AI Bridge request logs model filter (#22230)

This pull-request implements a simple filtering logic so that we're able
to pick which model the user actually used when logs were sent to AI
Bridge.

- Add `GET /aibridge/models` API endpoint that returns distinct model
names from AI Bridge interceptions, with pagination and search support
- New `ListAIBridgeModels` SQL query using case-sensitive prefix
matching (`LIKE model || '%'`) to allow B-tree index usage
- Hand-written `ListAuthorizedAIBridgeModels` in `modelqueries.go` for
RBAC authorization filter injection
- `AIBridgeModels` search query parser in searchquery/search.go
(defaults bare terms to the `model` field)
- dbauthz wrappers, dbmetrics, and dbmock implementations for the new
query

<img width="292" height="185" alt="image"
src="https://github.com/user-attachments/assets/134771df-2d26-4c54-acc4-27f58128b351"
/>
This commit is contained in:
Jake Howell
2026-02-26 02:40:45 +11:00
committed by GitHub
parent 1dec1ec4ad
commit d2787df442
20 changed files with 457 additions and 3 deletions
+7
View File
@@ -2864,6 +2864,13 @@ class ApiMethods {
await this.axios.get<TypesGen.AIBridgeListInterceptionsResponse>(url);
return response.data;
};
getAIBridgeModels = async (options: SearchParamOptions) => {
const url = getURLWithSearchParams("/api/v2/aibridge/models", options);
const response = await this.axios.get<string[]>(url);
return response.data;
};
}
export type TaskFeedbackRating = "good" | "okay" | "bad";
@@ -0,0 +1,78 @@
import { API } from "api/api";
import { ComboboxInput } from "components/Combobox/Combobox";
import {
type UseFilterMenuOptions,
useFilterMenu,
} from "components/Filter/menu";
import { SelectFilter } from "components/Filter/SelectFilter";
import type { FC } from "react";
import { AIBridgeModelIcon } from "../icons/AIBridgeModelIcon";
export const useModelFilterMenu = ({
value,
onChange,
enabled,
}: Pick<UseFilterMenuOptions, "value" | "onChange" | "enabled">) => {
return useFilterMenu({
id: "model",
getSelectedOption: async () => {
const modelsRes = await API.getAIBridgeModels({
q: value,
limit: 1,
});
const firstModel = modelsRes.at(0);
if (firstModel && firstModel === value) {
return {
label: firstModel,
value: firstModel,
startIcon: (
<AIBridgeModelIcon model={firstModel} className="size-icon-sm" />
),
};
}
return null;
},
getOptions: async (query) => {
const modelsRes = await API.getAIBridgeModels({
q: query,
limit: 25,
});
return modelsRes.map((model) => ({
label: model,
value: model,
startIcon: <AIBridgeModelIcon model={model} className="size-icon-sm" />,
}));
},
value,
onChange,
enabled,
});
};
export type ModelFilterMenu = ReturnType<typeof useModelFilterMenu>;
interface ModelFilterProps {
menu: ModelFilterMenu;
}
export const ModelFilter: FC<ModelFilterProps> = ({ menu }) => {
return (
<SelectFilter
label="Select model"
placeholder="All models"
emptyText="No models found"
options={menu.searchOptions}
onSelect={(option) => menu.selectOption(option)}
selectedOption={menu.selectedOption ?? undefined}
selectFilterSearch={
<ComboboxInput
placeholder="Search model..."
value={menu.query}
onValueChange={menu.setQuery}
/>
}
/>
);
};
@@ -1,7 +1,8 @@
import { Filter, MenuSkeleton, type useFilter } from "components/Filter/Filter";
import { type UserFilterMenu, UserMenu } from "components/Filter/UserFilter";
import type { FC } from "react";
import { ProviderFilter, type ProviderFilterMenu } from "./filter";
import { ModelFilter, type ModelFilterMenu } from "./ModelFilter";
import { ProviderFilter, type ProviderFilterMenu } from "./ProviderFilter";
interface RequestLogsFilterProps {
filter: ReturnType<typeof useFilter>;
@@ -9,6 +10,7 @@ interface RequestLogsFilterProps {
menus: {
user: UserFilterMenu;
provider: ProviderFilterMenu;
model: ModelFilterMenu;
};
}
@@ -37,6 +39,7 @@ export const RequestLogsFilter: FC<RequestLogsFilterProps> = ({
<>
<UserMenu menu={menus.user} placeholder="All initiators" />
<ProviderFilter menu={menus.provider} />
<ModelFilter menu={menus.model} />
</>
}
/>
@@ -8,7 +8,8 @@ import { RequirePermission } from "modules/permissions/RequirePermission";
import type { FC } from "react";
import { useSearchParams } from "react-router";
import { pageTitle } from "utils/page";
import { useProviderFilterMenu } from "./filter/filter";
import { useModelFilterMenu } from "./RequestLogsFilter/ModelFilter";
import { useProviderFilterMenu } from "./RequestLogsFilter/ProviderFilter";
import { RequestLogsPageView } from "./RequestLogsPageView";
const RequestLogsPage: FC = () => {
@@ -52,6 +53,15 @@ const RequestLogsPage: FC = () => {
}),
});
const modelMenu = useModelFilterMenu({
value: filter.values.model,
onChange: (option) =>
filter.update({
...filter.values,
model: option?.value,
}),
});
return (
<RequirePermission isFeatureVisible={hasPermission}>
<title>{pageTitle("Request Logs", "AI Bridge")}</title>
@@ -67,6 +77,7 @@ const RequestLogsPage: FC = () => {
menus: {
user: userMenu,
provider: providerMenu,
model: modelMenu,
},
}}
/>
@@ -26,6 +26,7 @@ const defaultFilterProps = getDefaultFilterProps<FilterProps>({
menus: {
user: MockMenu,
provider: MockMenu,
model: MockMenu,
},
});
@@ -14,7 +14,7 @@ import {
import { TableEmpty } from "components/TableEmpty/TableEmpty";
import { TableLoader } from "components/TableLoader/TableLoader";
import type { ComponentProps, FC } from "react";
import { RequestLogsFilter } from "./filter/RequestLogsFilter";
import { RequestLogsFilter } from "./RequestLogsFilter/RequestLogsFilter";
import { RequestLogsRow } from "./RequestLogsRow/RequestLogsRow";
interface RequestLogsPageViewProps {