mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+4
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user