first commit
This commit is contained in:
198
ui/lib/store/apis/baseApi.ts
Normal file
198
ui/lib/store/apis/baseApi.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { IS_ENTERPRISE } from "@/lib/constants/config";
|
||||
import { BifrostErrorResponse } from "@/lib/types/config";
|
||||
import { getApiBaseUrl } from "@/lib/utils/port";
|
||||
import { createBaseQueryWithRefresh } from "@enterprise/lib/store/utils/baseQueryWithRefresh";
|
||||
import { clearOAuthStorage } from "@enterprise/lib/store/utils/tokenManager";
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
|
||||
// Auth tokens are now stored in HTTP-only cookies (set by server)
|
||||
// No client-side token needed — handled by credentials: "include"
|
||||
export const getTokenFromStorage = (): Promise<string | null> => {
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
|
||||
// Helper function to set auth token
|
||||
// Non-enterprise: no-op — auth relies on HTTPOnly cookies set by the server
|
||||
// Enterprise: handled separately via tokenManager
|
||||
export const setAuthToken = (_token: string | null) => {
|
||||
// Non-enterprise auth is cookie-based; no client-side token storage needed.
|
||||
// Enterprise token management is handled by the tokenManager module.
|
||||
};
|
||||
|
||||
// Helper function to clear all auth-related storage
|
||||
export const clearAuthStorage = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Clear traditional auth token
|
||||
localStorage.removeItem("bifrost-auth-token");
|
||||
|
||||
// Clear enterprise OAuth tokens using tokenManager
|
||||
if (IS_ENTERPRISE) {
|
||||
clearOAuthStorage();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error clearing auth storage:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Define the base query with authentication headers
|
||||
const baseQuery = fetchBaseQuery({
|
||||
baseUrl: getApiBaseUrl(),
|
||||
credentials: "include",
|
||||
prepareHeaders: async (headers) => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
// Automatically include token from localStorage in Authorization header
|
||||
const token = await getTokenFromStorage();
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
});
|
||||
|
||||
// Wrap base query with enterprise refresh logic (or passthrough for non-enterprise)
|
||||
const baseQueryWithRefresh = createBaseQueryWithRefresh(baseQuery);
|
||||
|
||||
// Enhanced base query with error handling
|
||||
const baseQueryWithErrorHandling: typeof baseQueryWithRefresh = async (args: any, api: any, extraOptions: any) => {
|
||||
// First apply refresh logic (enterprise-specific, handles 401)
|
||||
const result = await baseQueryWithRefresh(args, api, extraOptions);
|
||||
|
||||
// Then handle other error types
|
||||
if (result.error) {
|
||||
const error = result.error as any;
|
||||
|
||||
// Handle 401 for non-enterprise (no refresh available)
|
||||
if (error?.status === 401 && !IS_ENTERPRISE) {
|
||||
clearAuthStorage();
|
||||
if (typeof window !== "undefined" && !window.location.pathname.includes("/login")) {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Handle specific error types
|
||||
if (error?.status === "FETCH_ERROR") {
|
||||
// Network error
|
||||
return {
|
||||
...result,
|
||||
error: {
|
||||
...error,
|
||||
data: {
|
||||
error: {
|
||||
message: "Network error: Unable to connect to the server",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Handle other errors with proper BifrostErrorResponse format
|
||||
if (error?.data) {
|
||||
const errorData = error.data as BifrostErrorResponse;
|
||||
if (errorData.error?.message) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback error message
|
||||
return {
|
||||
...result,
|
||||
error: {
|
||||
...error,
|
||||
data: {
|
||||
error: {
|
||||
message: "An unexpected error occurred",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Create the base API
|
||||
export const baseApi = createApi({
|
||||
reducerPath: "api",
|
||||
baseQuery: baseQueryWithErrorHandling,
|
||||
tagTypes: [
|
||||
"Logs",
|
||||
"MCPLogs",
|
||||
"Providers",
|
||||
"MCPClients",
|
||||
"Config",
|
||||
"CacheConfig",
|
||||
"VirtualKeys",
|
||||
"Teams",
|
||||
"Customers",
|
||||
"Budgets",
|
||||
"RateLimits",
|
||||
"UsageStats",
|
||||
"DebugStats",
|
||||
"HealthCheck",
|
||||
"DBKeys",
|
||||
"ProviderKeys",
|
||||
"Models",
|
||||
"BaseModels",
|
||||
"ModelConfigs",
|
||||
"ProviderGovernance",
|
||||
"Plugins",
|
||||
"SCIMProviders",
|
||||
"User",
|
||||
"Guardrails",
|
||||
"ClusterNodes",
|
||||
"Users",
|
||||
"GuardrailRules",
|
||||
"Roles",
|
||||
"Resources",
|
||||
"Operations",
|
||||
"Permissions",
|
||||
"APIKeys",
|
||||
"OAuth2Config",
|
||||
"RoutingRules",
|
||||
"PricingOverrides",
|
||||
"MCPToolGroups",
|
||||
"AuditLogs",
|
||||
"UserGovernance",
|
||||
"LargePayloadConfig",
|
||||
"Folders",
|
||||
"Prompts",
|
||||
"Versions",
|
||||
"Sessions",
|
||||
"AccessProfiles",
|
||||
"BusinessUnits",
|
||||
"PromptDeployments",
|
||||
],
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
|
||||
// Helper function to extract error message from RTK Query error
|
||||
export const getErrorMessage = (error: unknown): string => {
|
||||
if (error === undefined || error === null) {
|
||||
return "An unexpected error occurred";
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
if (
|
||||
typeof error === "object" &&
|
||||
error &&
|
||||
"data" in error &&
|
||||
error.data &&
|
||||
typeof error.data === "object" &&
|
||||
"error" in error.data &&
|
||||
error.data.error &&
|
||||
typeof error.data.error === "object" &&
|
||||
"message" in error.data.error &&
|
||||
typeof error.data.error.message === "string"
|
||||
) {
|
||||
return error.data.error.message.charAt(0).toUpperCase() + error.data.error.message.slice(1);
|
||||
}
|
||||
if (typeof error === "object" && error && "message" in error && typeof error.message === "string") {
|
||||
return error.message;
|
||||
}
|
||||
return "An unexpected error occurred";
|
||||
};
|
||||
110
ui/lib/store/apis/configApi.ts
Normal file
110
ui/lib/store/apis/configApi.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { IS_ENTERPRISE } from "@/lib/constants/config";
|
||||
import { BifrostConfig, GlobalProxyConfig, LatestReleaseResponse } from "@/lib/types/config";
|
||||
import axios from "axios";
|
||||
import { baseApi } from "./baseApi";
|
||||
|
||||
export const configApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Get core configuration
|
||||
getCoreConfig: builder.query<BifrostConfig, { fromDB?: boolean }>({
|
||||
query: ({ fromDB = false } = {}) => ({
|
||||
url: "/config",
|
||||
params: { from_db: fromDB },
|
||||
}),
|
||||
providesTags: ["Config"],
|
||||
}),
|
||||
|
||||
// Get version information
|
||||
getVersion: builder.query<string, void>({
|
||||
query: () => ({
|
||||
url: "/version",
|
||||
}),
|
||||
}),
|
||||
|
||||
// Get latest release from public site
|
||||
getLatestRelease: builder.query<LatestReleaseResponse, void>({
|
||||
queryFn: async (_arg, { signal }) => {
|
||||
try {
|
||||
const response = await axios.get("https://getbifrost.ai/latest-release", {
|
||||
timeout: 3000, // 3 second timeout
|
||||
signal,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
maxRedirects: 5,
|
||||
validateStatus: (status) => status >= 200 && status < 300,
|
||||
});
|
||||
const data = response.data as any;
|
||||
const normalized: LatestReleaseResponse = {
|
||||
name: data.name ?? data.tag ?? data.version ?? "",
|
||||
changelogUrl: data.changelogUrl ?? data.changelog_url ?? "",
|
||||
};
|
||||
return { data: normalized };
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
|
||||
console.warn("Latest release fetch timed out after 3s");
|
||||
return {
|
||||
error: {
|
||||
status: "TIMEOUT_ERROR",
|
||||
error: "Request timeout",
|
||||
data: { error: { message: "Request timeout" } },
|
||||
},
|
||||
};
|
||||
}
|
||||
console.error("Latest release fetch error:", error.message);
|
||||
} else {
|
||||
console.error("Latest release fetch error:", error);
|
||||
}
|
||||
return {
|
||||
error: {
|
||||
status: "FETCH_ERROR",
|
||||
error: String(error),
|
||||
data: { error: { message: "Network error" } },
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
keepUnusedDataFor: 300, // Cache for 5 minutes (seconds)
|
||||
}),
|
||||
// Update core configuration
|
||||
updateCoreConfig: builder.mutation<null, BifrostConfig>({
|
||||
query: (data) => ({
|
||||
url: "/config",
|
||||
method: "PUT",
|
||||
body: IS_ENTERPRISE ? { ...data, auth_config: undefined } : data,
|
||||
}),
|
||||
invalidatesTags: ["Config"],
|
||||
}),
|
||||
|
||||
// Update proxy configuration
|
||||
updateProxyConfig: builder.mutation<null, GlobalProxyConfig>({
|
||||
query: (data) => ({
|
||||
url: "/proxy-config",
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["Config"],
|
||||
}),
|
||||
|
||||
// Force a pricing sync immediately
|
||||
forcePricingSync: builder.mutation<null, void>({
|
||||
query: () => ({
|
||||
url: "/pricing/force-sync",
|
||||
method: "POST",
|
||||
}),
|
||||
invalidatesTags: ["Config"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetVersionQuery,
|
||||
useGetCoreConfigQuery,
|
||||
useUpdateCoreConfigMutation,
|
||||
useUpdateProxyConfigMutation,
|
||||
useForcePricingSyncMutation,
|
||||
useLazyGetCoreConfigQuery,
|
||||
useGetLatestReleaseQuery,
|
||||
useLazyGetLatestReleaseQuery,
|
||||
} = configApi;
|
||||
109
ui/lib/store/apis/devApi.ts
Normal file
109
ui/lib/store/apis/devApi.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { baseApi } from './baseApi'
|
||||
|
||||
// Memory statistics at a point in time
|
||||
export interface MemoryStats {
|
||||
alloc: number
|
||||
total_alloc: number
|
||||
heap_inuse: number
|
||||
heap_objects: number
|
||||
sys: number
|
||||
}
|
||||
|
||||
// CPU statistics
|
||||
export interface CPUStats {
|
||||
usage_percent: number
|
||||
user_time: number
|
||||
system_time: number
|
||||
}
|
||||
|
||||
// Runtime statistics
|
||||
export interface RuntimeStats {
|
||||
num_goroutine: number
|
||||
num_gc: number
|
||||
gc_pause_ns: number
|
||||
num_cpu: number
|
||||
gomaxprocs: number
|
||||
}
|
||||
|
||||
// Allocation info for top allocations
|
||||
export interface AllocationInfo {
|
||||
function: string
|
||||
file: string
|
||||
line: number
|
||||
bytes: number
|
||||
count: number
|
||||
stack: string[]
|
||||
}
|
||||
|
||||
// Single point in the metrics history
|
||||
export interface HistoryPoint {
|
||||
timestamp: string
|
||||
alloc: number
|
||||
heap_inuse: number
|
||||
goroutines: number
|
||||
gc_pause_ns: number
|
||||
cpu_percent: number
|
||||
}
|
||||
|
||||
// Complete pprof data response
|
||||
export interface PprofData {
|
||||
timestamp: string
|
||||
memory: MemoryStats
|
||||
cpu: CPUStats
|
||||
runtime: RuntimeStats
|
||||
top_allocations: AllocationInfo[]
|
||||
inuse_allocations: AllocationInfo[]
|
||||
history: HistoryPoint[]
|
||||
}
|
||||
|
||||
// Goroutine group representing goroutines with same stack trace
|
||||
export interface GoroutineGroup {
|
||||
count: number
|
||||
state: string
|
||||
wait_reason?: string
|
||||
wait_minutes?: number
|
||||
top_func: string
|
||||
stack: string[]
|
||||
category: 'background' | 'per-request' | 'unknown'
|
||||
}
|
||||
|
||||
// Goroutine health summary
|
||||
export interface GoroutineSummary {
|
||||
background: number
|
||||
per_request: number
|
||||
long_waiting: number
|
||||
potentially_stuck: number
|
||||
}
|
||||
|
||||
// Goroutine profile response
|
||||
export interface GoroutineProfile {
|
||||
timestamp: string
|
||||
total_goroutines: number
|
||||
groups: GoroutineGroup[]
|
||||
summary: GoroutineSummary
|
||||
}
|
||||
|
||||
export const devApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Get dev pprof data - polls every 10 seconds
|
||||
getDevPprof: builder.query<PprofData, void>({
|
||||
query: () => ({
|
||||
url: '/dev/pprof',
|
||||
}),
|
||||
}),
|
||||
// Get goroutine profile for leak detection
|
||||
getDevGoroutines: builder.query<GoroutineProfile, void>({
|
||||
query: () => ({
|
||||
url: '/dev/pprof/goroutines',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const {
|
||||
useGetDevPprofQuery,
|
||||
useLazyGetDevPprofQuery,
|
||||
useGetDevGoroutinesQuery,
|
||||
useLazyGetDevGoroutinesQuery,
|
||||
} = devApi
|
||||
|
||||
858
ui/lib/store/apis/governanceApi.ts
Normal file
858
ui/lib/store/apis/governanceApi.ts
Normal file
@@ -0,0 +1,858 @@
|
||||
import {
|
||||
Budget,
|
||||
CreateCustomerRequest,
|
||||
CreateModelConfigRequest,
|
||||
CreatePricingOverrideRequest,
|
||||
UpdatePricingOverrideRequest,
|
||||
CreateTeamRequest,
|
||||
CreateVirtualKeyRequest,
|
||||
Customer,
|
||||
DebugStatsResponse,
|
||||
GetBudgetsResponse,
|
||||
GetCustomersParams,
|
||||
GetCustomersResponse,
|
||||
GetModelConfigsParams,
|
||||
GetModelConfigsResponse,
|
||||
GetPricingOverridesResponse,
|
||||
GetProviderGovernanceResponse,
|
||||
GetRateLimitsResponse,
|
||||
GetTeamsParams,
|
||||
GetTeamsResponse,
|
||||
GetUsageStatsResponse,
|
||||
GetVirtualKeysParams,
|
||||
GetVirtualKeysResponse,
|
||||
HealthCheckResponse,
|
||||
ModelConfig,
|
||||
ProviderGovernance,
|
||||
PricingOverride,
|
||||
RateLimit,
|
||||
ResetUsageRequest,
|
||||
Team,
|
||||
UpdateBudgetRequest,
|
||||
UpdateCustomerRequest,
|
||||
UpdateModelConfigRequest,
|
||||
UpdateProviderGovernanceRequest,
|
||||
UpdateRateLimitRequest,
|
||||
UpdateTeamRequest,
|
||||
UpdateVirtualKeyRequest,
|
||||
VirtualKey,
|
||||
} from "@/lib/types/governance";
|
||||
import { baseApi } from "./baseApi";
|
||||
|
||||
type PricingOverrideQueryArgs = {
|
||||
scopeKind?: string;
|
||||
virtualKeyID?: string;
|
||||
providerID?: string;
|
||||
providerKeyID?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export const governanceApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Virtual Keys
|
||||
getVirtualKeys: builder.query<GetVirtualKeysResponse, GetVirtualKeysParams | void>({
|
||||
query: (params) => ({
|
||||
url: "/governance/virtual-keys",
|
||||
params: {
|
||||
...(params?.limit && { limit: params.limit }),
|
||||
...(params?.offset !== undefined && { offset: params.offset }),
|
||||
...(params?.search && { search: params.search }),
|
||||
...(params?.customer_id && { customer_id: params.customer_id }),
|
||||
...(params?.team_id && { team_id: params.team_id }),
|
||||
...(params?.exclude_access_profile_managed_virtual === true && {
|
||||
exclude_access_profile_managed_virtual: "true",
|
||||
}),
|
||||
...(params?.sort_by && { sort_by: params.sort_by }),
|
||||
...(params?.order && { order: params.order }),
|
||||
...(params?.export && { export: "true" }),
|
||||
},
|
||||
}),
|
||||
providesTags: ["VirtualKeys"],
|
||||
}),
|
||||
|
||||
getVirtualKey: builder.query<{ virtual_key: VirtualKey }, string>({
|
||||
query: (vkId) => `/governance/virtual-keys/${vkId}`,
|
||||
providesTags: (result, error, vkId) => [{ type: "VirtualKeys", id: vkId }],
|
||||
}),
|
||||
|
||||
createVirtualKey: builder.mutation<{ message: string; virtual_key: VirtualKey }, CreateVirtualKeyRequest>({
|
||||
query: (data) => ({
|
||||
url: "/governance/virtual-keys",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["VirtualKeys"],
|
||||
}),
|
||||
|
||||
updateVirtualKey: builder.mutation<{ message: string; virtual_key: VirtualKey }, { vkId: string; data: UpdateVirtualKeyRequest }>({
|
||||
query: ({ vkId, data }) => ({
|
||||
url: `/governance/virtual-keys/${vkId}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["VirtualKeys"],
|
||||
}),
|
||||
|
||||
deleteVirtualKey: builder.mutation<{ message: string }, string>({
|
||||
query: (vkId) => ({
|
||||
url: `/governance/virtual-keys/${vkId}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: ["VirtualKeys"],
|
||||
}),
|
||||
|
||||
// Teams
|
||||
getTeams: builder.query<GetTeamsResponse, GetTeamsParams | void>({
|
||||
query: (params) => ({
|
||||
url: "/governance/teams",
|
||||
params: {
|
||||
...(params?.limit && { limit: params.limit }),
|
||||
...(params?.offset !== undefined && { offset: params.offset }),
|
||||
...(params?.search && { search: params.search }),
|
||||
...(params?.customer_id && { customer_id: params.customer_id }),
|
||||
},
|
||||
}),
|
||||
providesTags: ["Teams"],
|
||||
}),
|
||||
|
||||
getTeam: builder.query<{ team: Team }, string>({
|
||||
query: (teamId) => `/governance/teams/${teamId}`,
|
||||
providesTags: (result, error, teamId) => [{ type: "Teams", id: teamId }],
|
||||
}),
|
||||
|
||||
createTeam: builder.mutation<{ message: string; team: Team }, CreateTeamRequest>({
|
||||
query: (data) => ({
|
||||
url: "/governance/teams",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted(arg, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getTeams" || entry?.status !== "fulfilled") continue;
|
||||
const search = entry.originalArgs?.search as string | undefined;
|
||||
if (search && !data.team.name.toLowerCase().includes(search.toLowerCase())) continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getTeams", entry.originalArgs, (draft) => {
|
||||
if (!draft.teams) draft.teams = [];
|
||||
draft.teams.unshift(data.team);
|
||||
draft.count = (draft.count || 0) + 1;
|
||||
draft.total_count = (draft.total_count || 0) + 1;
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
updateTeam: builder.mutation<{ message: string; team: Team }, { teamId: string; data: UpdateTeamRequest }>({
|
||||
query: ({ teamId, data }) => ({
|
||||
url: `/governance/teams/${teamId}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted({ teamId }, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getTeams" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getTeams", entry.originalArgs, (draft) => {
|
||||
if (!draft.teams) return;
|
||||
const index = draft.teams.findIndex((t) => t.id === teamId);
|
||||
if (index !== -1) {
|
||||
draft.teams[index] = data.team;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getTeam", teamId, (draft) => {
|
||||
draft.team = data.team;
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteTeam: builder.mutation<{ message: string }, string>({
|
||||
query: (teamId) => ({
|
||||
url: `/governance/teams/${teamId}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(teamId, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getTeams" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getTeams", entry.originalArgs, (draft) => {
|
||||
if (!draft.teams) return;
|
||||
const before = draft.teams.length;
|
||||
draft.teams = draft.teams.filter((t) => t.id !== teamId);
|
||||
if (draft.teams.length < before) {
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
draft.total_count = Math.max(0, (draft.total_count || 0) - 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// Customers
|
||||
getCustomers: builder.query<GetCustomersResponse, GetCustomersParams | void>({
|
||||
query: (params) => ({
|
||||
url: "/governance/customers",
|
||||
params: {
|
||||
...(params?.limit && { limit: params.limit }),
|
||||
...(params?.offset !== undefined && { offset: params.offset }),
|
||||
...(params?.search && { search: params.search }),
|
||||
},
|
||||
}),
|
||||
providesTags: ["Customers"],
|
||||
}),
|
||||
|
||||
getCustomer: builder.query<{ customer: Customer }, string>({
|
||||
query: (customerId) => `/governance/customers/${customerId}`,
|
||||
providesTags: (result, error, customerId) => [{ type: "Customers", id: customerId }],
|
||||
}),
|
||||
|
||||
createCustomer: builder.mutation<{ message: string; customer: Customer }, CreateCustomerRequest>({
|
||||
query: (data) => ({
|
||||
url: "/governance/customers",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted(arg, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getCustomers" || entry?.status !== "fulfilled") continue;
|
||||
const search = entry.originalArgs?.search as string | undefined;
|
||||
if (search && !data.customer.name.toLowerCase().includes(search.toLowerCase())) continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getCustomers", entry.originalArgs, (draft) => {
|
||||
if (!draft.customers) draft.customers = [];
|
||||
draft.customers.unshift(data.customer);
|
||||
draft.count = (draft.count || 0) + 1;
|
||||
draft.total_count = (draft.total_count || 0) + 1;
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
updateCustomer: builder.mutation<{ message: string; customer: Customer }, { customerId: string; data: UpdateCustomerRequest }>({
|
||||
query: ({ customerId, data }) => ({
|
||||
url: `/governance/customers/${customerId}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted({ customerId }, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getCustomers" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getCustomers", entry.originalArgs, (draft) => {
|
||||
if (!draft.customers) return;
|
||||
const index = draft.customers.findIndex((c) => c.id === customerId);
|
||||
if (index !== -1) {
|
||||
draft.customers[index] = data.customer;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getCustomer", customerId, (draft) => {
|
||||
draft.customer = data.customer;
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteCustomer: builder.mutation<{ message: string }, string>({
|
||||
query: (customerId) => ({
|
||||
url: `/governance/customers/${customerId}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(customerId, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getCustomers" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getCustomers", entry.originalArgs, (draft) => {
|
||||
if (!draft.customers) return;
|
||||
const before = draft.customers.length;
|
||||
draft.customers = draft.customers.filter((c) => c.id !== customerId);
|
||||
if (draft.customers.length < before) {
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
draft.total_count = Math.max(0, (draft.total_count || 0) - 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// Budgets
|
||||
getBudgets: builder.query<GetBudgetsResponse, void>({
|
||||
query: () => "/governance/budgets",
|
||||
providesTags: ["Budgets"],
|
||||
}),
|
||||
|
||||
getBudget: builder.query<{ budget: Budget }, string>({
|
||||
query: (budgetId) => `/governance/budgets/${budgetId}`,
|
||||
providesTags: (result, error, budgetId) => [{ type: "Budgets", id: budgetId }],
|
||||
}),
|
||||
|
||||
updateBudget: builder.mutation<{ message: string; budget: Budget }, { budgetId: string; data: UpdateBudgetRequest }>({
|
||||
query: ({ budgetId, data }) => ({
|
||||
url: `/governance/budgets/${budgetId}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted({ budgetId }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getBudgets", undefined, (draft) => {
|
||||
if (!draft.budgets) return;
|
||||
const index = draft.budgets.findIndex((b) => b.id === budgetId);
|
||||
if (index !== -1) {
|
||||
draft.budgets[index] = data.budget;
|
||||
}
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getBudget", budgetId, (draft) => {
|
||||
draft.budget = data.budget;
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteBudget: builder.mutation<{ message: string }, string>({
|
||||
query: (budgetId) => ({
|
||||
url: `/governance/budgets/${budgetId}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(budgetId, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getBudgets", undefined, (draft) => {
|
||||
if (!draft.budgets) return;
|
||||
draft.budgets = draft.budgets.filter((b) => b.id !== budgetId);
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// Rate Limits
|
||||
getRateLimits: builder.query<GetRateLimitsResponse, void>({
|
||||
query: () => "/governance/rate-limits",
|
||||
providesTags: ["RateLimits"],
|
||||
}),
|
||||
|
||||
getRateLimit: builder.query<{ rate_limit: RateLimit }, string>({
|
||||
query: (rateLimitId) => `/governance/rate-limits/${rateLimitId}`,
|
||||
providesTags: (result, error, rateLimitId) => [{ type: "RateLimits", id: rateLimitId }],
|
||||
}),
|
||||
|
||||
updateRateLimit: builder.mutation<{ message: string; rate_limit: RateLimit }, { rateLimitId: string; data: UpdateRateLimitRequest }>({
|
||||
query: ({ rateLimitId, data }) => ({
|
||||
url: `/governance/rate-limits/${rateLimitId}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted({ rateLimitId }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getRateLimits", undefined, (draft) => {
|
||||
if (!draft.rate_limits) return;
|
||||
const index = draft.rate_limits.findIndex((r) => r.id === rateLimitId);
|
||||
if (index !== -1) {
|
||||
draft.rate_limits[index] = data.rate_limit;
|
||||
}
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getRateLimit", rateLimitId, (draft) => {
|
||||
draft.rate_limit = data.rate_limit;
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteRateLimit: builder.mutation<{ message: string }, string>({
|
||||
query: (rateLimitId) => ({
|
||||
url: `/governance/rate-limits/${rateLimitId}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(rateLimitId, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getRateLimits", undefined, (draft) => {
|
||||
if (!draft.rate_limits) return;
|
||||
draft.rate_limits = draft.rate_limits.filter((r) => r.id !== rateLimitId);
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// Usage Stats
|
||||
getUsageStats: builder.query<GetUsageStatsResponse, { virtualKeyId?: string }>({
|
||||
query: ({ virtualKeyId } = {}) => ({
|
||||
url: "/governance/usage-stats",
|
||||
params: virtualKeyId ? { virtual_key_id: virtualKeyId } : {},
|
||||
}),
|
||||
providesTags: ["UsageStats"],
|
||||
}),
|
||||
|
||||
resetUsage: builder.mutation<{ message: string }, ResetUsageRequest>({
|
||||
query: (data) => ({
|
||||
url: "/governance/usage-reset",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["UsageStats"],
|
||||
}),
|
||||
|
||||
// Debug endpoints
|
||||
getGovernanceDebugStats: builder.query<DebugStatsResponse, void>({
|
||||
query: () => "/governance/debug/stats",
|
||||
providesTags: ["DebugStats"],
|
||||
}),
|
||||
|
||||
getGovernanceHealth: builder.query<HealthCheckResponse, void>({
|
||||
query: () => "/governance/debug/health",
|
||||
providesTags: ["HealthCheck"],
|
||||
}),
|
||||
|
||||
// Model Configs
|
||||
getModelConfigs: builder.query<GetModelConfigsResponse, GetModelConfigsParams | void>({
|
||||
query: (params) => ({
|
||||
url: "/governance/model-configs",
|
||||
params: {
|
||||
...(params?.limit && { limit: params.limit }),
|
||||
...(params?.offset !== undefined && { offset: params.offset }),
|
||||
...(params?.search && { search: params.search }),
|
||||
},
|
||||
}),
|
||||
providesTags: ["ModelConfigs"],
|
||||
}),
|
||||
|
||||
getModelConfig: builder.query<{ model_config: ModelConfig }, string>({
|
||||
query: (id) => `/governance/model-configs/${id}`,
|
||||
providesTags: (result, error, id) => [{ type: "ModelConfigs", id }],
|
||||
}),
|
||||
|
||||
createModelConfig: builder.mutation<{ message: string; model_config: ModelConfig }, CreateModelConfigRequest>({
|
||||
query: (data) => ({
|
||||
url: "/governance/model-configs",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted(arg, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getModelConfigs" || entry?.status !== "fulfilled") continue;
|
||||
const search = entry.originalArgs?.search as string | undefined;
|
||||
if (search && !data.model_config.model_name.toLowerCase().includes(search.toLowerCase())) continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getModelConfigs", entry.originalArgs, (draft) => {
|
||||
if (!draft.model_configs) draft.model_configs = [];
|
||||
draft.model_configs.unshift(data.model_config);
|
||||
draft.count = (draft.count || 0) + 1;
|
||||
draft.total_count = (draft.total_count || 0) + 1;
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed, do nothing - error handling bubbled up
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
updateModelConfig: builder.mutation<{ message: string; model_config: ModelConfig }, { id: string; data: UpdateModelConfigRequest }>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/governance/model-configs/${id}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted({ id }, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getModelConfigs" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getModelConfigs", entry.originalArgs, (draft) => {
|
||||
if (!draft.model_configs) return;
|
||||
const index = draft.model_configs.findIndex((mc) => mc.id === id);
|
||||
if (index !== -1) {
|
||||
draft.model_configs[index] = data.model_config;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getModelConfig", id, (draft) => {
|
||||
draft.model_config = data.model_config;
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteModelConfig: builder.mutation<{ message: string }, string>({
|
||||
query: (id) => ({
|
||||
url: `/governance/model-configs/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(id, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getModelConfigs" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getModelConfigs", entry.originalArgs, (draft) => {
|
||||
if (!draft.model_configs) return;
|
||||
const before = draft.model_configs.length;
|
||||
draft.model_configs = draft.model_configs.filter((mc) => mc.id !== id);
|
||||
if (draft.model_configs.length < before) {
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
draft.total_count = Math.max(0, (draft.total_count || 0) - 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
getPricingOverrides: builder.query<GetPricingOverridesResponse, PricingOverrideQueryArgs | void>({
|
||||
query: (params) => ({
|
||||
url: "/governance/pricing-overrides",
|
||||
params: {
|
||||
scope_kind: params?.scopeKind,
|
||||
virtual_key_id: params?.virtualKeyID,
|
||||
provider_id: params?.providerID,
|
||||
provider_key_id: params?.providerKeyID,
|
||||
...(params?.limit !== undefined && { limit: params.limit }),
|
||||
...(params?.offset !== undefined && { offset: params.offset }),
|
||||
...(params?.search && { search: params.search }),
|
||||
},
|
||||
}),
|
||||
providesTags: ["PricingOverrides"],
|
||||
}),
|
||||
|
||||
createPricingOverride: builder.mutation<{ message: string; pricing_override: PricingOverride }, CreatePricingOverrideRequest>({
|
||||
query: (data) => ({
|
||||
url: "/governance/pricing-overrides",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted(_arg, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const created = data.pricing_override;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getPricingOverrides" || entry?.status !== "fulfilled") continue;
|
||||
const args: PricingOverrideQueryArgs = entry.originalArgs ?? {};
|
||||
const matchesQuery =
|
||||
(!args.scopeKind || args.scopeKind === created.scope_kind) &&
|
||||
(!args.virtualKeyID || args.virtualKeyID === created.virtual_key_id) &&
|
||||
(!args.providerID || args.providerID === created.provider_id) &&
|
||||
(!args.providerKeyID || args.providerKeyID === created.provider_key_id) &&
|
||||
(!args.search || created.name?.toLowerCase().includes(args.search.toLowerCase()));
|
||||
if (!matchesQuery) continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getPricingOverrides", entry.originalArgs, (draft) => {
|
||||
if (!draft.pricing_overrides) draft.pricing_overrides = [];
|
||||
if (!args.offset || args.offset === 0) {
|
||||
draft.pricing_overrides.unshift(created);
|
||||
draft.count = (draft.count || 0) + 1;
|
||||
draft.total_count = (draft.total_count || 0) + 1;
|
||||
} else {
|
||||
draft.total_count = (draft.total_count || 0) + 1;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
updatePricingOverride: builder.mutation<
|
||||
{ message: string; pricing_override: PricingOverride },
|
||||
{ id: string; data: UpdatePricingOverrideRequest }
|
||||
>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/governance/pricing-overrides/${id}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted({ id }, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const updated = data.pricing_override;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getPricingOverrides" || entry?.status !== "fulfilled") continue;
|
||||
const args: PricingOverrideQueryArgs = entry.originalArgs ?? {};
|
||||
const matchesQuery =
|
||||
(!args.scopeKind || args.scopeKind === updated.scope_kind) &&
|
||||
(!args.virtualKeyID || args.virtualKeyID === updated.virtual_key_id) &&
|
||||
(!args.providerID || args.providerID === updated.provider_id) &&
|
||||
(!args.providerKeyID || args.providerKeyID === updated.provider_key_id);
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getPricingOverrides", entry.originalArgs, (draft) => {
|
||||
if (!draft.pricing_overrides) return;
|
||||
const index = draft.pricing_overrides.findIndex((o) => o.id === id);
|
||||
if (index === -1) return;
|
||||
if (matchesQuery) {
|
||||
draft.pricing_overrides[index] = updated;
|
||||
} else {
|
||||
// Override no longer belongs in this filtered list
|
||||
draft.pricing_overrides.splice(index, 1);
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
draft.total_count = Math.max(0, (draft.total_count || 0) - 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deletePricingOverride: builder.mutation<{ message: string }, string>({
|
||||
query: (id) => ({
|
||||
url: `/governance/pricing-overrides/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(id, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getPricingOverrides" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getPricingOverrides", entry.originalArgs, (draft) => {
|
||||
if (!draft.pricing_overrides) return;
|
||||
const before = draft.pricing_overrides.length;
|
||||
draft.pricing_overrides = draft.pricing_overrides.filter((o) => o.id !== id);
|
||||
const removed = before - draft.pricing_overrides.length;
|
||||
if (removed > 0) {
|
||||
draft.count = Math.max(0, (draft.count || 0) - removed);
|
||||
draft.total_count = Math.max(0, (draft.total_count || 0) - removed);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// Provider Governance
|
||||
getProviderGovernance: builder.query<GetProviderGovernanceResponse, { fromMemory?: boolean } | void>({
|
||||
query: (params) => ({
|
||||
url: "/governance/providers",
|
||||
params: { from_memory: params?.fromMemory ?? false },
|
||||
}),
|
||||
providesTags: ["ProviderGovernance"],
|
||||
}),
|
||||
|
||||
updateProviderGovernance: builder.mutation<
|
||||
{ message: string; provider: ProviderGovernance },
|
||||
{ provider: string; data: UpdateProviderGovernanceRequest }
|
||||
>({
|
||||
query: ({ provider, data }) => ({
|
||||
url: `/governance/providers/${encodeURIComponent(provider)}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted({ provider }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data } = await queryFulfilled;
|
||||
const variants = [undefined, { fromMemory: true }] as const;
|
||||
for (const variant of variants) {
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getProviderGovernance", variant, (draft) => {
|
||||
if (!draft.providers) draft.providers = [];
|
||||
const index = draft.providers.findIndex((p) => p.provider === provider);
|
||||
if (index !== -1) {
|
||||
draft.providers[index] = data.provider;
|
||||
} else {
|
||||
// New provider governance - add to list
|
||||
draft.providers.push(data.provider);
|
||||
draft.count = (draft.count || 0) + 1;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteProviderGovernance: builder.mutation<{ message: string }, string>({
|
||||
query: (provider) => ({
|
||||
url: `/governance/providers/${encodeURIComponent(provider)}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(provider, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
const variants = [undefined, { fromMemory: true }] as const;
|
||||
for (const variant of variants) {
|
||||
dispatch(
|
||||
governanceApi.util.updateQueryData("getProviderGovernance", variant, (draft) => {
|
||||
if (!draft.providers) return;
|
||||
draft.providers = draft.providers.filter((p) => p.provider !== provider);
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Mutation failed
|
||||
}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
// Virtual Keys
|
||||
useGetVirtualKeysQuery,
|
||||
useGetVirtualKeyQuery,
|
||||
useCreateVirtualKeyMutation,
|
||||
useUpdateVirtualKeyMutation,
|
||||
useDeleteVirtualKeyMutation,
|
||||
|
||||
// Teams
|
||||
useGetTeamsQuery,
|
||||
useGetTeamQuery,
|
||||
useCreateTeamMutation,
|
||||
useUpdateTeamMutation,
|
||||
useDeleteTeamMutation,
|
||||
|
||||
// Customers
|
||||
useGetCustomersQuery,
|
||||
useGetCustomerQuery,
|
||||
useCreateCustomerMutation,
|
||||
useUpdateCustomerMutation,
|
||||
useDeleteCustomerMutation,
|
||||
|
||||
// Budgets
|
||||
useGetBudgetsQuery,
|
||||
useGetBudgetQuery,
|
||||
useUpdateBudgetMutation,
|
||||
useDeleteBudgetMutation,
|
||||
|
||||
// Rate Limits
|
||||
useGetRateLimitsQuery,
|
||||
useGetRateLimitQuery,
|
||||
useUpdateRateLimitMutation,
|
||||
useDeleteRateLimitMutation,
|
||||
|
||||
// Usage Stats
|
||||
useGetUsageStatsQuery,
|
||||
useResetUsageMutation,
|
||||
|
||||
// Debug
|
||||
useGetGovernanceDebugStatsQuery,
|
||||
useGetGovernanceHealthQuery,
|
||||
|
||||
// Model Configs
|
||||
useGetModelConfigsQuery,
|
||||
useGetModelConfigQuery,
|
||||
useCreateModelConfigMutation,
|
||||
useUpdateModelConfigMutation,
|
||||
useDeleteModelConfigMutation,
|
||||
useGetPricingOverridesQuery,
|
||||
useCreatePricingOverrideMutation,
|
||||
useUpdatePricingOverrideMutation,
|
||||
useDeletePricingOverrideMutation,
|
||||
|
||||
// Provider Governance
|
||||
useGetProviderGovernanceQuery,
|
||||
useUpdateProviderGovernanceMutation,
|
||||
useDeleteProviderGovernanceMutation,
|
||||
|
||||
// Lazy queries
|
||||
useLazyGetVirtualKeysQuery,
|
||||
useLazyGetVirtualKeyQuery,
|
||||
useLazyGetTeamsQuery,
|
||||
useLazyGetTeamQuery,
|
||||
useLazyGetCustomersQuery,
|
||||
useLazyGetCustomerQuery,
|
||||
useLazyGetBudgetsQuery,
|
||||
useLazyGetBudgetQuery,
|
||||
useLazyGetRateLimitsQuery,
|
||||
useLazyGetRateLimitQuery,
|
||||
useLazyGetUsageStatsQuery,
|
||||
useLazyGetGovernanceDebugStatsQuery,
|
||||
useLazyGetGovernanceHealthQuery,
|
||||
useLazyGetModelConfigsQuery,
|
||||
useLazyGetProviderGovernanceQuery,
|
||||
} = governanceApi;
|
||||
14
ui/lib/store/apis/index.ts
Normal file
14
ui/lib/store/apis/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Base API
|
||||
export { baseApi, clearAuthStorage, getErrorMessage, setAuthToken } from "./baseApi";
|
||||
|
||||
// API slices and hooks
|
||||
export * from "./configApi";
|
||||
export * from "./devApi";
|
||||
export * from "./governanceApi";
|
||||
export * from "./logsApi";
|
||||
export * from "./mcpApi";
|
||||
export * from "./mcpLogsApi";
|
||||
export * from "./pluginsApi";
|
||||
export * from "./providersApi";
|
||||
export * from "./promptsApi";
|
||||
export * from "./sessionApi";
|
||||
361
ui/lib/store/apis/logsApi.ts
Normal file
361
ui/lib/store/apis/logsApi.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { RedactedDBKey, VirtualKey } from "@/lib/types/governance";
|
||||
import {
|
||||
CostHistogramResponse,
|
||||
LatencyHistogramResponse,
|
||||
LogEntry,
|
||||
LogFilters,
|
||||
LogSessionDetailResponse,
|
||||
LogSessionSummaryResponse,
|
||||
LogsHistogramResponse,
|
||||
LogStats,
|
||||
ModelHistogramResponse,
|
||||
ModelRankingsResponse,
|
||||
Pagination,
|
||||
ProviderCostHistogramResponse,
|
||||
ProviderLatencyHistogramResponse,
|
||||
ProviderTokenHistogramResponse,
|
||||
RecalculateCostResponse,
|
||||
TokenHistogramResponse,
|
||||
} from "@/lib/types/logs";
|
||||
import { baseApi } from "./baseApi";
|
||||
import { RoutingRule } from "@/lib/types/routingRules";
|
||||
|
||||
// Helper function to build filter params
|
||||
function buildFilterParams(filters: LogFilters): Record<string, string | number> {
|
||||
const params: Record<string, string | number> = {};
|
||||
|
||||
if (filters.parent_request_id) {
|
||||
params.parent_request_id = filters.parent_request_id;
|
||||
}
|
||||
if (filters.providers && filters.providers.length > 0) {
|
||||
params.providers = filters.providers.join(",");
|
||||
}
|
||||
if (filters.models && filters.models.length > 0) {
|
||||
params.models = filters.models.join(",");
|
||||
}
|
||||
if (filters.aliases && filters.aliases.length > 0) {
|
||||
params.aliases = filters.aliases.join(",");
|
||||
}
|
||||
if (filters.status && filters.status.length > 0) {
|
||||
params.status = filters.status.join(",");
|
||||
}
|
||||
if (filters.objects && filters.objects.length > 0) {
|
||||
params.objects = filters.objects.join(",");
|
||||
}
|
||||
if (filters.selected_key_ids && filters.selected_key_ids.length > 0) {
|
||||
params.selected_key_ids = filters.selected_key_ids.join(",");
|
||||
}
|
||||
if (filters.virtual_key_ids && filters.virtual_key_ids.length > 0) {
|
||||
params.virtual_key_ids = filters.virtual_key_ids.join(",");
|
||||
}
|
||||
if (filters.routing_rule_ids && filters.routing_rule_ids.length > 0) {
|
||||
params.routing_rule_ids = filters.routing_rule_ids.join(",");
|
||||
}
|
||||
if (filters.routing_engine_used && filters.routing_engine_used.length > 0) {
|
||||
params.routing_engine_used = filters.routing_engine_used.join(",");
|
||||
}
|
||||
if (filters.start_time) params.start_time = filters.start_time;
|
||||
if (filters.end_time) params.end_time = filters.end_time;
|
||||
if (filters.min_latency !== undefined) params.min_latency = filters.min_latency;
|
||||
if (filters.max_latency !== undefined) params.max_latency = filters.max_latency;
|
||||
if (filters.min_tokens !== undefined) params.min_tokens = filters.min_tokens;
|
||||
if (filters.max_tokens !== undefined) params.max_tokens = filters.max_tokens;
|
||||
if (filters.missing_cost_only) params.missing_cost_only = "true";
|
||||
if (filters.content_search) params.content_search = filters.content_search;
|
||||
if (filters.user_ids && filters.user_ids.length > 0) {
|
||||
params.user_ids = filters.user_ids.join(",");
|
||||
}
|
||||
if (filters.team_ids && filters.team_ids.length > 0) {
|
||||
params.team_ids = filters.team_ids.join(",");
|
||||
}
|
||||
if (filters.customer_ids && filters.customer_ids.length > 0) {
|
||||
params.customer_ids = filters.customer_ids.join(",");
|
||||
}
|
||||
if (filters.business_unit_ids && filters.business_unit_ids.length > 0) {
|
||||
params.business_unit_ids = filters.business_unit_ids.join(",");
|
||||
}
|
||||
if (filters.metadata_filters) {
|
||||
for (const [key, value] of Object.entries(filters.metadata_filters)) {
|
||||
params[`metadata_${key}`] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
export const logsApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Get logs with filters and pagination
|
||||
getLogs: builder.query<
|
||||
{
|
||||
logs: LogEntry[];
|
||||
pagination: Pagination;
|
||||
stats: LogStats;
|
||||
has_logs: boolean;
|
||||
},
|
||||
{
|
||||
filters: LogFilters;
|
||||
pagination: Pagination;
|
||||
}
|
||||
>({
|
||||
query: ({ filters, pagination }) => ({
|
||||
url: "/logs",
|
||||
params: {
|
||||
limit: pagination.limit,
|
||||
offset: pagination.offset,
|
||||
sort_by: pagination.sort_by,
|
||||
order: pagination.order,
|
||||
...buildFilterParams(filters),
|
||||
},
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
getLogSessionById: builder.query<
|
||||
LogSessionDetailResponse,
|
||||
{
|
||||
sessionId: string;
|
||||
pagination: Pick<Pagination, "limit" | "offset" | "order">;
|
||||
}
|
||||
>({
|
||||
query: ({ sessionId, pagination }) => ({
|
||||
url: `/logs/sessions/${encodeURIComponent(sessionId)}`,
|
||||
params: {
|
||||
limit: pagination.limit,
|
||||
offset: pagination.offset,
|
||||
order: pagination.order,
|
||||
},
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
getLogSessionSummaryById: builder.query<LogSessionSummaryResponse, string>({
|
||||
query: (sessionId) => ({
|
||||
url: `/logs/sessions/${encodeURIComponent(sessionId)}/summary`,
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get logs statistics with filters
|
||||
getLogsStats: builder.query<
|
||||
LogStats,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/stats",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get logs histogram with filters
|
||||
getLogsHistogram: builder.query<
|
||||
LogsHistogramResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/histogram",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get token usage histogram with filters
|
||||
getLogsTokenHistogram: builder.query<
|
||||
TokenHistogramResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/histogram/tokens",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get cost histogram with filters and model breakdown
|
||||
getLogsCostHistogram: builder.query<
|
||||
CostHistogramResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/histogram/cost",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get model usage histogram with filters
|
||||
getLogsModelHistogram: builder.query<
|
||||
ModelHistogramResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/histogram/models",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get latency histogram with percentiles
|
||||
getLogsLatencyHistogram: builder.query<
|
||||
LatencyHistogramResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/histogram/latency",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get provider cost histogram with provider breakdown
|
||||
getLogsProviderCostHistogram: builder.query<
|
||||
ProviderCostHistogramResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/histogram/cost/by-provider",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get provider token histogram with provider breakdown
|
||||
getLogsProviderTokenHistogram: builder.query<
|
||||
ProviderTokenHistogramResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/histogram/tokens/by-provider",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get provider latency histogram with provider breakdown
|
||||
getLogsProviderLatencyHistogram: builder.query<
|
||||
ProviderLatencyHistogramResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/histogram/latency/by-provider",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get model rankings with trends
|
||||
getModelRankings: builder.query<
|
||||
ModelRankingsResponse,
|
||||
{
|
||||
filters: LogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/logs/rankings",
|
||||
params: buildFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get dropped requests count
|
||||
getDroppedRequests: builder.query<{ dropped_requests: number }, void>({
|
||||
query: () => "/logs/dropped",
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get available models
|
||||
getAvailableFilterData: builder.query<
|
||||
{
|
||||
models: string[];
|
||||
aliases: string[];
|
||||
selected_keys: RedactedDBKey[];
|
||||
virtual_keys: VirtualKey[];
|
||||
routing_rules: RoutingRule[];
|
||||
routing_engines: string[];
|
||||
metadata_keys: Record<string, string[]>;
|
||||
},
|
||||
void
|
||||
>({
|
||||
query: () => "/logs/filterdata",
|
||||
providesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Delete logs by their IDs
|
||||
deleteLogs: builder.mutation<void, { ids: string[] }>({
|
||||
query: ({ ids }) => ({
|
||||
url: "/logs",
|
||||
method: "DELETE",
|
||||
body: { ids },
|
||||
}),
|
||||
invalidatesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
recalculateLogCosts: builder.mutation<RecalculateCostResponse, { filters: LogFilters; limit?: number }>({
|
||||
query: ({ filters, limit }) => ({
|
||||
url: "/logs/recalculate-cost",
|
||||
method: "POST",
|
||||
body: { filters, limit },
|
||||
}),
|
||||
invalidatesTags: ["Logs"],
|
||||
}),
|
||||
|
||||
// Get a single log entry by ID (includes raw_request and raw_response)
|
||||
getLogById: builder.query<LogEntry, string>({
|
||||
query: (id) => `/logs/${encodeURIComponent(id)}`,
|
||||
providesTags: (result, error, id) => [{ type: "Logs", id }],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetLogsQuery,
|
||||
useGetLogsStatsQuery,
|
||||
useGetLogsHistogramQuery,
|
||||
useGetLogsTokenHistogramQuery,
|
||||
useGetLogsCostHistogramQuery,
|
||||
useGetLogsModelHistogramQuery,
|
||||
useGetLogsLatencyHistogramQuery,
|
||||
useGetLogsProviderCostHistogramQuery,
|
||||
useGetLogsProviderTokenHistogramQuery,
|
||||
useGetLogsProviderLatencyHistogramQuery,
|
||||
useGetLogSessionSummaryByIdQuery,
|
||||
useGetDroppedRequestsQuery,
|
||||
useGetAvailableFilterDataQuery,
|
||||
useLazyGetLogSessionByIdQuery,
|
||||
useLazyGetLogsQuery,
|
||||
useLazyGetLogsStatsQuery,
|
||||
useLazyGetLogsHistogramQuery,
|
||||
useLazyGetLogsTokenHistogramQuery,
|
||||
useLazyGetLogsCostHistogramQuery,
|
||||
useLazyGetLogsModelHistogramQuery,
|
||||
useLazyGetLogsLatencyHistogramQuery,
|
||||
useLazyGetLogsProviderCostHistogramQuery,
|
||||
useLazyGetLogsProviderTokenHistogramQuery,
|
||||
useLazyGetLogsProviderLatencyHistogramQuery,
|
||||
useLazyGetModelRankingsQuery,
|
||||
useLazyGetDroppedRequestsQuery,
|
||||
useLazyGetAvailableFilterDataQuery,
|
||||
useDeleteLogsMutation,
|
||||
useRecalculateLogCostsMutation,
|
||||
useLazyGetLogByIdQuery,
|
||||
useGetLogByIdQuery,
|
||||
} = logsApi;
|
||||
150
ui/lib/store/apis/mcpApi.ts
Normal file
150
ui/lib/store/apis/mcpApi.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
CreateMCPClientRequest,
|
||||
GetMCPClientsParams,
|
||||
GetMCPClientsResponse,
|
||||
MCPClient,
|
||||
OAuthFlowResponse,
|
||||
OAuthStatusResponse,
|
||||
UpdateMCPClientRequest,
|
||||
} from "@/lib/types/mcp";
|
||||
import { baseApi } from "./baseApi";
|
||||
|
||||
type CreateMCPClientResponse = { status: "success"; message: string } | OAuthFlowResponse;
|
||||
|
||||
export const mcpApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Get MCP clients with pagination
|
||||
getMCPClients: builder.query<GetMCPClientsResponse, GetMCPClientsParams | void>({
|
||||
query: (params) => ({
|
||||
url: "/mcp/clients",
|
||||
params: {
|
||||
...(params?.limit && { limit: params.limit }),
|
||||
...(params?.offset !== undefined && { offset: params.offset }),
|
||||
...(params?.search && { search: params.search }),
|
||||
},
|
||||
}),
|
||||
providesTags: ["MCPClients"],
|
||||
}),
|
||||
|
||||
// Create new MCP client
|
||||
createMCPClient: builder.mutation<CreateMCPClientResponse, CreateMCPClientRequest>({
|
||||
query: (data) => ({
|
||||
url: "/mcp/client",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted(arg, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
// MCP create may return an OAuth flow response, so we can't optimistically
|
||||
// add the client — just invalidate to refetch
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getMCPClients" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(mcpApi.util.invalidateTags(["MCPClients"]));
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Update existing MCP client
|
||||
updateMCPClient: builder.mutation<any, { id: string; data: UpdateMCPClientRequest }>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/mcp/client/${id}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted({ id, data }, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getMCPClients" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
mcpApi.util.updateQueryData("getMCPClients", entry.originalArgs, (draft) => {
|
||||
if (!draft.clients) return;
|
||||
const index = draft.clients.findIndex((c) => c.config.client_id === id);
|
||||
if (index !== -1) {
|
||||
// Merge the updated fields into the existing client
|
||||
if (data.name !== undefined) draft.clients[index].config.name = data.name;
|
||||
if (data.is_code_mode_client !== undefined) draft.clients[index].config.is_code_mode_client = data.is_code_mode_client;
|
||||
if (data.headers !== undefined) draft.clients[index].config.headers = data.headers;
|
||||
if (data.tools_to_execute !== undefined) draft.clients[index].config.tools_to_execute = data.tools_to_execute;
|
||||
if (data.tools_to_auto_execute !== undefined)
|
||||
draft.clients[index].config.tools_to_auto_execute = data.tools_to_auto_execute;
|
||||
if (data.is_ping_available !== undefined) draft.clients[index].config.is_ping_available = data.is_ping_available;
|
||||
if (data.tool_pricing !== undefined) draft.clients[index].config.tool_pricing = data.tool_pricing;
|
||||
if (data.tool_sync_interval !== undefined) draft.clients[index].config.tool_sync_interval = data.tool_sync_interval;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete MCP client
|
||||
deleteMCPClient: builder.mutation<any, string>({
|
||||
query: (id) => ({
|
||||
url: `/mcp/client/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(id, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getMCPClients" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
mcpApi.util.updateQueryData("getMCPClients", entry.originalArgs, (draft) => {
|
||||
if (!draft.clients) return;
|
||||
const before = draft.clients.length;
|
||||
draft.clients = draft.clients.filter((c) => c.config.client_id !== id);
|
||||
if (draft.clients.length < before) {
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
draft.total_count = Math.max(0, (draft.total_count || 0) - 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Reconnect MCP client
|
||||
reconnectMCPClient: builder.mutation<any, string>({
|
||||
query: (id) => ({
|
||||
url: `/mcp/client/${id}/reconnect`,
|
||||
method: "POST",
|
||||
}),
|
||||
invalidatesTags: ["MCPClients"],
|
||||
}),
|
||||
|
||||
// Get OAuth config status (for polling)
|
||||
getOAuthConfigStatus: builder.query<OAuthStatusResponse, string>({
|
||||
query: (oauthConfigId) => `/oauth/config/${oauthConfigId}/status`,
|
||||
providesTags: (result, error, id) => [{ type: "OAuth2Config", id }],
|
||||
}),
|
||||
|
||||
// Complete OAuth flow for MCP client
|
||||
completeOAuthFlow: builder.mutation<{ status: string; message: string }, string>({
|
||||
query: (oauthConfigId) => ({
|
||||
url: `/mcp/client/${oauthConfigId}/complete-oauth`,
|
||||
method: "POST",
|
||||
}),
|
||||
invalidatesTags: ["MCPClients"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetMCPClientsQuery,
|
||||
useCreateMCPClientMutation,
|
||||
useUpdateMCPClientMutation,
|
||||
useDeleteMCPClientMutation,
|
||||
useReconnectMCPClientMutation,
|
||||
useLazyGetMCPClientsQuery,
|
||||
useLazyGetOAuthConfigStatusQuery,
|
||||
useCompleteOAuthFlowMutation,
|
||||
} = mcpApi;
|
||||
189
ui/lib/store/apis/mcpLogsApi.ts
Normal file
189
ui/lib/store/apis/mcpLogsApi.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
MCPToolLogEntry,
|
||||
MCPToolLogFilters,
|
||||
MCPToolLogStats,
|
||||
MCPToolLogFilterData,
|
||||
MCPHistogramResponse,
|
||||
MCPCostHistogramResponse,
|
||||
MCPTopToolsResponse,
|
||||
Pagination,
|
||||
} from "@/lib/types/logs";
|
||||
import { baseApi } from "./baseApi";
|
||||
|
||||
// Helper function to build MCP histogram filter params
|
||||
function buildMCPFilterParams(filters: MCPToolLogFilters): Record<string, string | number> {
|
||||
const params: Record<string, string | number> = {};
|
||||
if (filters.tool_names && filters.tool_names.length > 0) {
|
||||
params.tool_names = filters.tool_names.join(",");
|
||||
}
|
||||
if (filters.server_labels && filters.server_labels.length > 0) {
|
||||
params.server_labels = filters.server_labels.join(",");
|
||||
}
|
||||
if (filters.status && filters.status.length > 0) {
|
||||
params.status = filters.status.join(",");
|
||||
}
|
||||
if (filters.virtual_key_ids && filters.virtual_key_ids.length > 0) {
|
||||
params.virtual_key_ids = filters.virtual_key_ids.join(",");
|
||||
}
|
||||
if (filters.llm_request_ids && filters.llm_request_ids.length > 0) {
|
||||
params.llm_request_ids = filters.llm_request_ids.join(",");
|
||||
}
|
||||
if (filters.start_time) params.start_time = filters.start_time;
|
||||
if (filters.end_time) params.end_time = filters.end_time;
|
||||
if (filters.min_latency !== undefined) params.min_latency = filters.min_latency;
|
||||
if (filters.max_latency !== undefined) params.max_latency = filters.max_latency;
|
||||
if (filters.content_search) params.content_search = filters.content_search;
|
||||
return params;
|
||||
}
|
||||
|
||||
export const mcpLogsApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Get MCP tool logs with filters and pagination
|
||||
getMCPLogs: builder.query<
|
||||
{
|
||||
logs: MCPToolLogEntry[];
|
||||
pagination: Pagination;
|
||||
stats: MCPToolLogStats;
|
||||
has_logs: boolean;
|
||||
},
|
||||
{
|
||||
filters: MCPToolLogFilters;
|
||||
pagination: Pagination;
|
||||
}
|
||||
>({
|
||||
query: ({ filters, pagination }) => {
|
||||
const params: Record<string, string | number> = {
|
||||
limit: pagination.limit,
|
||||
offset: pagination.offset,
|
||||
sort_by: pagination.sort_by,
|
||||
order: pagination.order,
|
||||
};
|
||||
|
||||
// Add filters to params if they exist
|
||||
if (filters.tool_names && filters.tool_names.length > 0) {
|
||||
params.tool_names = filters.tool_names.join(",");
|
||||
}
|
||||
if (filters.server_labels && filters.server_labels.length > 0) {
|
||||
params.server_labels = filters.server_labels.join(",");
|
||||
}
|
||||
if (filters.status && filters.status.length > 0) {
|
||||
params.status = filters.status.join(",");
|
||||
}
|
||||
if (filters.virtual_key_ids && filters.virtual_key_ids.length > 0) {
|
||||
params.virtual_key_ids = filters.virtual_key_ids.join(",");
|
||||
}
|
||||
if (filters.llm_request_ids && filters.llm_request_ids.length > 0) {
|
||||
params.llm_request_ids = filters.llm_request_ids.join(",");
|
||||
}
|
||||
if (filters.start_time) params.start_time = filters.start_time;
|
||||
if (filters.end_time) params.end_time = filters.end_time;
|
||||
if (filters.min_latency) params.min_latency = filters.min_latency;
|
||||
if (filters.max_latency) params.max_latency = filters.max_latency;
|
||||
if (filters.content_search) params.content_search = filters.content_search;
|
||||
|
||||
return {
|
||||
url: "/mcp-logs",
|
||||
params,
|
||||
};
|
||||
},
|
||||
providesTags: ["MCPLogs"],
|
||||
}),
|
||||
|
||||
// Get MCP tool logs statistics with filters
|
||||
getMCPLogsStats: builder.query<
|
||||
MCPToolLogStats,
|
||||
{
|
||||
filters: MCPToolLogFilters;
|
||||
}
|
||||
>({
|
||||
query: ({ filters }) => {
|
||||
const params: Record<string, string | number> = {};
|
||||
|
||||
// Add filters to params if they exist
|
||||
if (filters.tool_names && filters.tool_names.length > 0) {
|
||||
params.tool_names = filters.tool_names.join(",");
|
||||
}
|
||||
if (filters.server_labels && filters.server_labels.length > 0) {
|
||||
params.server_labels = filters.server_labels.join(",");
|
||||
}
|
||||
if (filters.status && filters.status.length > 0) {
|
||||
params.status = filters.status.join(",");
|
||||
}
|
||||
if (filters.virtual_key_ids && filters.virtual_key_ids.length > 0) {
|
||||
params.virtual_key_ids = filters.virtual_key_ids.join(",");
|
||||
}
|
||||
if (filters.llm_request_ids && filters.llm_request_ids.length > 0) {
|
||||
params.llm_request_ids = filters.llm_request_ids.join(",");
|
||||
}
|
||||
if (filters.start_time) params.start_time = filters.start_time;
|
||||
if (filters.end_time) params.end_time = filters.end_time;
|
||||
if (filters.min_latency) params.min_latency = filters.min_latency;
|
||||
if (filters.max_latency) params.max_latency = filters.max_latency;
|
||||
if (filters.content_search) params.content_search = filters.content_search;
|
||||
|
||||
return {
|
||||
url: "/mcp-logs/stats",
|
||||
params,
|
||||
};
|
||||
},
|
||||
providesTags: ["MCPLogs"],
|
||||
}),
|
||||
|
||||
// Get available filter data (tool names, server labels)
|
||||
getMCPAvailableFilterData: builder.query<MCPToolLogFilterData, void>({
|
||||
query: () => "/mcp-logs/filterdata",
|
||||
providesTags: ["MCPLogs"],
|
||||
}),
|
||||
|
||||
// Get MCP tool call volume histogram
|
||||
getMCPHistogram: builder.query<MCPHistogramResponse, { filters: MCPToolLogFilters }>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/mcp-logs/histogram",
|
||||
params: buildMCPFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["MCPLogs"],
|
||||
}),
|
||||
|
||||
// Get MCP cost histogram
|
||||
getMCPCostHistogram: builder.query<MCPCostHistogramResponse, { filters: MCPToolLogFilters }>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/mcp-logs/histogram/cost",
|
||||
params: buildMCPFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["MCPLogs"],
|
||||
}),
|
||||
|
||||
// Get top MCP tools by call count
|
||||
getMCPTopTools: builder.query<MCPTopToolsResponse, { filters: MCPToolLogFilters }>({
|
||||
query: ({ filters }) => ({
|
||||
url: "/mcp-logs/histogram/top-tools",
|
||||
params: buildMCPFilterParams(filters),
|
||||
}),
|
||||
providesTags: ["MCPLogs"],
|
||||
}),
|
||||
|
||||
// Delete MCP tool logs by their IDs
|
||||
deleteMCPLogs: builder.mutation<void, { ids: string[] }>({
|
||||
query: ({ ids }) => ({
|
||||
url: "/mcp-logs",
|
||||
method: "DELETE",
|
||||
body: { ids },
|
||||
}),
|
||||
invalidatesTags: ["MCPLogs"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetMCPLogsQuery,
|
||||
useGetMCPLogsStatsQuery,
|
||||
useGetMCPAvailableFilterDataQuery,
|
||||
useGetMCPAvailableFilterDataQuery: useGetMCPLogsFilterDataQuery,
|
||||
useLazyGetMCPLogsQuery,
|
||||
useLazyGetMCPLogsStatsQuery,
|
||||
useLazyGetMCPAvailableFilterDataQuery,
|
||||
useLazyGetMCPHistogramQuery,
|
||||
useLazyGetMCPCostHistogramQuery,
|
||||
useLazyGetMCPTopToolsQuery,
|
||||
useDeleteMCPLogsMutation,
|
||||
} = mcpLogsApi;
|
||||
96
ui/lib/store/apis/pluginsApi.ts
Normal file
96
ui/lib/store/apis/pluginsApi.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { CreatePluginRequest, Plugin, PluginsResponse, UpdatePluginRequest } from "@/lib/types/plugins";
|
||||
import { baseApi } from "./baseApi";
|
||||
|
||||
export const pluginsApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Get all plugins
|
||||
getPlugins: builder.query<Plugin[], void>({
|
||||
query: () => "/plugins",
|
||||
providesTags: ["Plugins"],
|
||||
transformResponse: (response: PluginsResponse) => response.plugins || [],
|
||||
}),
|
||||
|
||||
// Get a single plugin
|
||||
getPlugin: builder.query<Plugin, string>({
|
||||
query: (name) => `/plugins/${name}`,
|
||||
providesTags: (result, error, name) => [{ type: "Plugins", id: name }],
|
||||
}),
|
||||
|
||||
// Create new plugin
|
||||
createPlugin: builder.mutation<Plugin, CreatePluginRequest>({
|
||||
query: (data) => ({
|
||||
url: "/plugins",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
transformResponse: (response: { message: string; plugin: Plugin }) => response.plugin,
|
||||
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: newPlugin } = await queryFulfilled;
|
||||
dispatch(
|
||||
pluginsApi.util.updateQueryData("getPlugins", undefined, (draft) => {
|
||||
draft.push(newPlugin);
|
||||
}),
|
||||
);
|
||||
// Also update the individual plugin cache
|
||||
dispatch(pluginsApi.util.updateQueryData("getPlugin", newPlugin.name, () => newPlugin));
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Update existing plugin
|
||||
updatePlugin: builder.mutation<Plugin, { name: string; data: UpdatePluginRequest }>({
|
||||
query: ({ name, data }) => ({
|
||||
url: `/plugins/${name}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
transformResponse: (response: { message: string; plugin: Plugin }) => response.plugin,
|
||||
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: updatedPlugin } = await queryFulfilled;
|
||||
dispatch(
|
||||
pluginsApi.util.updateQueryData("getPlugins", undefined, (draft) => {
|
||||
const index = draft.findIndex((p) => p.name === arg.name);
|
||||
if (index !== -1) {
|
||||
draft[index] = updatedPlugin;
|
||||
}
|
||||
}),
|
||||
);
|
||||
// Also update the individual plugin cache
|
||||
dispatch(pluginsApi.util.updateQueryData("getPlugin", arg.name, () => updatedPlugin));
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete plugin
|
||||
deletePlugin: builder.mutation<Plugin, string>({
|
||||
query: (name) => ({
|
||||
url: `/plugins/${name}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(pluginName, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
dispatch(
|
||||
pluginsApi.util.updateQueryData("getPlugins", undefined, (draft) => {
|
||||
const index = draft.findIndex((p) => p.name === pluginName);
|
||||
if (index !== -1) {
|
||||
draft.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetPluginsQuery,
|
||||
useGetPluginQuery,
|
||||
useCreatePluginMutation,
|
||||
useUpdatePluginMutation,
|
||||
useDeletePluginMutation,
|
||||
useLazyGetPluginsQuery,
|
||||
} = pluginsApi;
|
||||
276
ui/lib/store/apis/promptsApi.ts
Normal file
276
ui/lib/store/apis/promptsApi.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { baseApi } from "@/lib/store/apis/baseApi";
|
||||
import {
|
||||
CommitSessionRequest,
|
||||
CommitSessionResponse,
|
||||
CreateFolderRequest,
|
||||
CreateFolderResponse,
|
||||
CreatePromptRequest,
|
||||
CreatePromptResponse,
|
||||
CreateSessionRequest,
|
||||
CreateSessionResponse,
|
||||
CreateVersionRequest,
|
||||
CreateVersionResponse,
|
||||
DeleteFolderResponse,
|
||||
DeletePromptResponse,
|
||||
DeleteSessionResponse,
|
||||
DeleteVersionResponse,
|
||||
GetFolderResponse,
|
||||
GetFoldersResponse,
|
||||
GetPromptResponse,
|
||||
GetPromptsResponse,
|
||||
GetSessionResponse,
|
||||
GetSessionsResponse,
|
||||
GetVersionResponse,
|
||||
GetVersionsResponse,
|
||||
UpdateFolderRequest,
|
||||
UpdateFolderResponse,
|
||||
UpdatePromptRequest,
|
||||
UpdatePromptResponse,
|
||||
RenameSessionRequest,
|
||||
RenameSessionResponse,
|
||||
UpdateSessionRequest,
|
||||
UpdateSessionResponse,
|
||||
} from "@/lib/types/prompts";
|
||||
|
||||
// Inject Prompt Repository endpoints into baseApi
|
||||
export const promptsApi = baseApi.injectEndpoints({
|
||||
overrideExisting: true,
|
||||
endpoints: (builder) => ({
|
||||
// Get all folders
|
||||
getFolders: builder.query<GetFoldersResponse, void>({
|
||||
query: () => "/prompt-repo/folders",
|
||||
providesTags: ["Folders"],
|
||||
}),
|
||||
|
||||
// Get single folder
|
||||
getFolder: builder.query<GetFolderResponse, string>({
|
||||
query: (id) => `/prompt-repo/folders/${id}`,
|
||||
providesTags: (result, error, id) => [{ type: "Folders", id }],
|
||||
}),
|
||||
|
||||
// Create folder
|
||||
createFolder: builder.mutation<CreateFolderResponse, CreateFolderRequest>({
|
||||
query: (data) => ({
|
||||
url: "/prompt-repo/folders",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["Folders"],
|
||||
}),
|
||||
|
||||
// Update folder
|
||||
updateFolder: builder.mutation<UpdateFolderResponse, { id: string; data: UpdateFolderRequest }>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/prompt-repo/folders/${id}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: (result, error, { id }) => ["Folders", { type: "Folders", id }],
|
||||
}),
|
||||
|
||||
// Delete folder
|
||||
deleteFolder: builder.mutation<DeleteFolderResponse, string>({
|
||||
query: (id) => ({
|
||||
url: `/prompt-repo/folders/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: (result, error, id) => ["Folders", { type: "Folders", id }, "Prompts"],
|
||||
}),
|
||||
|
||||
// Get all prompts (optionally filtered by folder)
|
||||
getPrompts: builder.query<GetPromptsResponse, { folderId?: string } | void>({
|
||||
query: (params) => {
|
||||
const queryParams = params && params.folderId ? `?folder_id=${params.folderId}` : "";
|
||||
return `/prompt-repo/prompts${queryParams}`;
|
||||
},
|
||||
providesTags: ["Prompts"],
|
||||
}),
|
||||
|
||||
// Get single prompt
|
||||
getPrompt: builder.query<GetPromptResponse, string>({
|
||||
query: (id) => `/prompt-repo/prompts/${id}`,
|
||||
providesTags: (result, error, id) => [{ type: "Prompts", id }],
|
||||
}),
|
||||
|
||||
// Create prompt
|
||||
createPrompt: builder.mutation<CreatePromptResponse, CreatePromptRequest>({
|
||||
query: (data) => ({
|
||||
url: "/prompt-repo/prompts",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: ["Prompts", "Folders"],
|
||||
}),
|
||||
|
||||
// Update prompt
|
||||
updatePrompt: builder.mutation<UpdatePromptResponse, { id: string; data: UpdatePromptRequest }>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/prompt-repo/prompts/${id}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: (result, error, { id }) => ["Prompts", { type: "Prompts", id }],
|
||||
async onQueryStarted({ id, data }, { dispatch, queryFulfilled }) {
|
||||
// Optimistic update on the prompts list cache
|
||||
const patchResult = dispatch(
|
||||
promptsApi.util.updateQueryData("getPrompts", undefined, (draft) => {
|
||||
const prompt = draft.prompts.find((p) => p.id === id);
|
||||
if (prompt) {
|
||||
if (data.name !== undefined) prompt.name = data.name;
|
||||
if ("folder_id" in data) prompt.folder_id = data.folder_id ?? undefined;
|
||||
}
|
||||
}),
|
||||
);
|
||||
try {
|
||||
await queryFulfilled;
|
||||
} catch {
|
||||
patchResult.undo();
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete prompt
|
||||
deletePrompt: builder.mutation<DeletePromptResponse, string>({
|
||||
query: (id) => ({
|
||||
url: `/prompt-repo/prompts/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: (result, error, id) => ["Prompts", { type: "Prompts", id }, "Folders", "Versions", "Sessions"],
|
||||
}),
|
||||
|
||||
// Get all versions for a prompt
|
||||
getVersions: builder.query<GetVersionsResponse, string>({
|
||||
query: (promptId) => `/prompt-repo/prompts/${promptId}/versions`,
|
||||
providesTags: (result, error, promptId) => [{ type: "Versions", id: promptId }],
|
||||
}),
|
||||
|
||||
// Get single version
|
||||
getPromptVersion: builder.query<GetVersionResponse, number>({
|
||||
query: (id) => `/prompt-repo/versions/${id}`,
|
||||
providesTags: (result, error, id) => [{ type: "Versions", id }],
|
||||
}),
|
||||
|
||||
// Create version
|
||||
createVersion: builder.mutation<CreateVersionResponse, { promptId: string; data: CreateVersionRequest }>({
|
||||
query: ({ promptId, data }) => ({
|
||||
url: `/prompt-repo/prompts/${promptId}/versions`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: (result, error, { promptId }) => ["Prompts", { type: "Prompts", id: promptId }, { type: "Versions", id: promptId }],
|
||||
}),
|
||||
|
||||
// Delete version
|
||||
deleteVersion: builder.mutation<DeleteVersionResponse, { id: number; promptId: string }>({
|
||||
query: ({ id }) => ({
|
||||
url: `/prompt-repo/versions/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: (result, error, { id, promptId }) => [
|
||||
"Prompts",
|
||||
{ type: "Prompts", id: promptId },
|
||||
{ type: "Versions", id: promptId },
|
||||
{ type: "Versions", id },
|
||||
],
|
||||
}),
|
||||
|
||||
// Get all sessions for a prompt
|
||||
getSessions: builder.query<GetSessionsResponse, string>({
|
||||
query: (promptId) => `/prompt-repo/prompts/${promptId}/sessions`,
|
||||
providesTags: (result, error, promptId) => [{ type: "Sessions", id: promptId }],
|
||||
}),
|
||||
|
||||
// Get single session
|
||||
getSession: builder.query<GetSessionResponse, number>({
|
||||
query: (id) => `/prompt-repo/sessions/${id}`,
|
||||
providesTags: (result, error, id) => [{ type: "Sessions", id }],
|
||||
}),
|
||||
|
||||
// Create session
|
||||
createSession: builder.mutation<CreateSessionResponse, { promptId: string; data: CreateSessionRequest }>({
|
||||
query: ({ promptId, data }) => ({
|
||||
url: `/prompt-repo/prompts/${promptId}/sessions`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: (result, error, { promptId }) => [{ type: "Sessions", id: promptId }],
|
||||
}),
|
||||
|
||||
// Update session
|
||||
updateSession: builder.mutation<UpdateSessionResponse, { id: number; promptId: string; data: UpdateSessionRequest }>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/prompt-repo/sessions/${id}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: (result, error, { id, promptId }) => [
|
||||
{ type: "Sessions", id },
|
||||
{ type: "Sessions", id: promptId },
|
||||
],
|
||||
}),
|
||||
|
||||
// Delete session
|
||||
deleteSession: builder.mutation<DeleteSessionResponse, { id: number; promptId: string }>({
|
||||
query: ({ id }) => ({
|
||||
url: `/prompt-repo/sessions/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: (result, error, { id, promptId }) => [
|
||||
{ type: "Sessions", id: promptId },
|
||||
{ type: "Sessions", id },
|
||||
],
|
||||
}),
|
||||
|
||||
// Rename session
|
||||
renameSession: builder.mutation<RenameSessionResponse, { id: number; promptId: string; data: RenameSessionRequest }>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/prompt-repo/sessions/${id}/rename`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: (result, error, { id, promptId }) => [
|
||||
{ type: "Sessions", id: promptId },
|
||||
{ type: "Sessions", id },
|
||||
],
|
||||
}),
|
||||
|
||||
// Commit session as new version
|
||||
commitSession: builder.mutation<CommitSessionResponse, { id: number; promptId: string; data: CommitSessionRequest }>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/prompt-repo/sessions/${id}/commit`,
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
invalidatesTags: (result, error, { promptId }) => ["Prompts", { type: "Prompts", id: promptId }, { type: "Versions", id: promptId }],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
// Folders
|
||||
useGetFoldersQuery,
|
||||
useGetFolderQuery,
|
||||
useCreateFolderMutation,
|
||||
useUpdateFolderMutation,
|
||||
useDeleteFolderMutation,
|
||||
// Prompts
|
||||
useGetPromptsQuery,
|
||||
useGetPromptQuery,
|
||||
useCreatePromptMutation,
|
||||
useUpdatePromptMutation,
|
||||
useDeletePromptMutation,
|
||||
// Versions
|
||||
useGetVersionsQuery,
|
||||
useGetPromptVersionQuery,
|
||||
useLazyGetPromptVersionQuery,
|
||||
useCreateVersionMutation,
|
||||
useDeleteVersionMutation,
|
||||
// Sessions
|
||||
useGetSessionsQuery,
|
||||
useGetSessionQuery,
|
||||
useCreateSessionMutation,
|
||||
useUpdateSessionMutation,
|
||||
useDeleteSessionMutation,
|
||||
useRenameSessionMutation,
|
||||
useCommitSessionMutation,
|
||||
} = promptsApi;
|
||||
375
ui/lib/store/apis/providersApi.ts
Normal file
375
ui/lib/store/apis/providersApi.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
import {
|
||||
AddProviderRequest,
|
||||
CreateProviderKeyRequest,
|
||||
ListProviderKeysResponse,
|
||||
ListProvidersResponse,
|
||||
ModelProvider,
|
||||
ModelProviderKey,
|
||||
ModelProviderName,
|
||||
UpdateProviderRequest,
|
||||
UpdateProviderKeyRequest,
|
||||
} from "@/lib/types/config";
|
||||
import { DBKey } from "@/lib/types/governance";
|
||||
import { baseApi } from "./baseApi";
|
||||
|
||||
function sortProviders(a: ModelProvider, b: ModelProvider) {
|
||||
const aIsCustom = !!a.custom_provider_config;
|
||||
const bIsCustom = !!b.custom_provider_config;
|
||||
if (aIsCustom !== bIsCustom) return aIsCustom ? 1 : -1;
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
// Types for models API
|
||||
export interface ModelResponse {
|
||||
name: string;
|
||||
provider: string;
|
||||
accessible_by_keys?: string[];
|
||||
}
|
||||
|
||||
export interface ListModelsResponse {
|
||||
models: ModelResponse[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface GetModelsRequest {
|
||||
query?: string;
|
||||
provider?: string;
|
||||
keys?: string[];
|
||||
vks?: string[];
|
||||
limit?: number;
|
||||
unfiltered?: boolean;
|
||||
}
|
||||
|
||||
export interface GetBaseModelsRequest {
|
||||
query?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface ModelDatasheetParameter {
|
||||
id: string;
|
||||
label: string;
|
||||
helpText?: string;
|
||||
type: string;
|
||||
accesorKey?: string;
|
||||
default?: any;
|
||||
multiple?: boolean;
|
||||
range?: { min: number; max: number; step?: number };
|
||||
array?: { type: string; maxElements?: number; minElements?: number };
|
||||
options?: { label: string; value: string; subFields?: ModelDatasheetParameter[] }[];
|
||||
}
|
||||
|
||||
export interface ModelDatasheetResponse {
|
||||
model_parameters?: ModelDatasheetParameter[];
|
||||
max_input_tokens?: number;
|
||||
max_output_tokens?: number;
|
||||
max_tokens?: number;
|
||||
mode?: string;
|
||||
provider?: string;
|
||||
base_model?: string;
|
||||
supports_vision?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ListBaseModelsResponse {
|
||||
models: string[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
type UpdateProviderMutationArg = UpdateProviderRequest & { name: ModelProviderName };
|
||||
|
||||
const DEFAULT_MODEL_PARAMETERS: ModelDatasheetResponse = {
|
||||
mode: "chat",
|
||||
base_model: "default",
|
||||
model_parameters: [
|
||||
{
|
||||
id: "temperature",
|
||||
label: "Temperature",
|
||||
helpText:
|
||||
"What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.",
|
||||
type: "number",
|
||||
range: { min: 0, max: 2, step: 0.01 },
|
||||
},
|
||||
{
|
||||
id: "max_tokens",
|
||||
label: "Max Tokens",
|
||||
helpText: "The maximum number of tokens that can be generated in the Result.",
|
||||
type: "number",
|
||||
range: { min: 1, max: 8192, step: 1 },
|
||||
},
|
||||
{
|
||||
id: "stream",
|
||||
label: "Stream",
|
||||
helpText:
|
||||
"The stream parameter in the API controls whether the response is sent in incremental updates, like tokenized data as it's generated, or as a complete result in one go.",
|
||||
type: "boolean",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const providersApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Get all providers
|
||||
getProviders: builder.query<ModelProvider[], void>({
|
||||
query: () => "/providers",
|
||||
transformResponse: (response: ListProvidersResponse): ModelProvider[] => (response.providers ?? []).sort(sortProviders),
|
||||
providesTags: ["Providers"],
|
||||
}),
|
||||
|
||||
// Get single provider
|
||||
getProvider: builder.query<ModelProvider, string>({
|
||||
query: (provider) => `/providers/${encodeURIComponent(provider)}`,
|
||||
providesTags: (result, error, provider) => [{ type: "Providers", id: provider }],
|
||||
}),
|
||||
|
||||
getProviderKeys: builder.query<ModelProviderKey[], string>({
|
||||
query: (provider) => `/providers/${encodeURIComponent(provider)}/keys`,
|
||||
transformResponse: (response: ListProviderKeysResponse) => response.keys ?? [],
|
||||
providesTags: (result, error, provider) => [{ type: "ProviderKeys", id: provider }],
|
||||
}),
|
||||
|
||||
getProviderKey: builder.query<ModelProviderKey, { provider: string; keyId: string }>({
|
||||
query: ({ provider, keyId }) => `/providers/${encodeURIComponent(provider)}/keys/${encodeURIComponent(keyId)}`,
|
||||
providesTags: (result, error, { provider }) => [{ type: "ProviderKeys", id: provider }],
|
||||
}),
|
||||
|
||||
// Create new provider
|
||||
createProvider: builder.mutation<ModelProvider, AddProviderRequest>({
|
||||
query: (data) => ({
|
||||
url: "/providers",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: newProvider } = await queryFulfilled;
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getProviders", undefined, (draft) => {
|
||||
draft.push(newProvider);
|
||||
draft.sort(sortProviders);
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Update existing provider
|
||||
updateProvider: builder.mutation<ModelProvider, UpdateProviderMutationArg>({
|
||||
query: ({ name, ...body }) => ({
|
||||
url: `/providers/${encodeURIComponent(name)}`,
|
||||
method: "PUT",
|
||||
body,
|
||||
}),
|
||||
invalidatesTags: (result, error, arg) => [{ type: "ProviderKeys", id: arg.name }, "DBKeys", "VirtualKeys"],
|
||||
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: updatedProvider } = await queryFulfilled;
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getProviders", undefined, (draft) => {
|
||||
const index = draft.findIndex((p) => p.name === arg.name);
|
||||
if (index !== -1) {
|
||||
draft[index] = updatedProvider;
|
||||
}
|
||||
}),
|
||||
);
|
||||
dispatch(providersApi.util.updateQueryData("getProvider", arg.name, () => updatedProvider));
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
createProviderKey: builder.mutation<ModelProviderKey, { provider: string; key: CreateProviderKeyRequest }>({
|
||||
query: ({ provider, key }) => ({
|
||||
url: `/providers/${encodeURIComponent(provider)}/keys`,
|
||||
method: "POST",
|
||||
body: key,
|
||||
}),
|
||||
async onQueryStarted({ provider }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: newKey } = await queryFulfilled;
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getProviderKeys", provider, (draft) => {
|
||||
const exists = draft.some((k) => k.id === newKey.id);
|
||||
if (!exists) {
|
||||
draft.push(newKey);
|
||||
}
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getAllKeys", undefined, (draft) => {
|
||||
const exists = draft.some((k) => k.key_id === newKey.id);
|
||||
if (!exists) {
|
||||
draft.push({
|
||||
key_id: newKey.id,
|
||||
name: newKey.name,
|
||||
provider_id: "",
|
||||
models: newKey.models ?? [],
|
||||
provider: provider as ModelProviderName,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
updateProviderKey: builder.mutation<ModelProviderKey, { provider: string; keyId: string; key: UpdateProviderKeyRequest }>({
|
||||
query: ({ provider, keyId, key }) => ({
|
||||
url: `/providers/${encodeURIComponent(provider)}/keys/${encodeURIComponent(keyId)}`,
|
||||
method: "PUT",
|
||||
body: key,
|
||||
}),
|
||||
async onQueryStarted({ provider, keyId }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: updatedKey } = await queryFulfilled;
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getProviderKeys", provider, (draft) => {
|
||||
const index = draft.findIndex((key) => key.id === keyId);
|
||||
if (index !== -1) {
|
||||
draft[index] = updatedKey;
|
||||
}
|
||||
}),
|
||||
);
|
||||
dispatch(providersApi.util.updateQueryData("getProviderKey", { provider, keyId }, () => updatedKey));
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getAllKeys", undefined, (draft) => {
|
||||
const index = draft.findIndex((k) => k.key_id === keyId);
|
||||
if (index !== -1) {
|
||||
draft[index] = { ...draft[index], name: updatedKey.name, models: updatedKey.models ?? [] };
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
deleteProviderKey: builder.mutation<ModelProviderKey, { provider: string; keyId: string }>({
|
||||
query: ({ provider, keyId }) => ({
|
||||
url: `/providers/${encodeURIComponent(provider)}/keys/${encodeURIComponent(keyId)}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted({ provider, keyId }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getProviderKeys", provider, (draft) => {
|
||||
const index = draft.findIndex((key) => key.id === keyId);
|
||||
if (index !== -1) {
|
||||
draft.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getAllKeys", undefined, (draft) => {
|
||||
const index = draft.findIndex((k) => k.key_id === keyId);
|
||||
if (index !== -1) {
|
||||
draft.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete provider
|
||||
deleteProvider: builder.mutation<ModelProviderName, string>({
|
||||
query: (provider) => ({
|
||||
url: `/providers/${encodeURIComponent(provider)}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
invalidatesTags: ["VirtualKeys"],
|
||||
async onQueryStarted(providerName, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getProviders", undefined, (draft) => {
|
||||
const index = draft.findIndex((p) => p.name === providerName);
|
||||
if (index !== -1) {
|
||||
draft.splice(index, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getProviderKeys", providerName, (draft) => {
|
||||
draft.splice(0, draft.length);
|
||||
}),
|
||||
);
|
||||
dispatch(
|
||||
providersApi.util.updateQueryData("getAllKeys", undefined, (draft) => draft.filter((key) => key.provider !== providerName)),
|
||||
);
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Get all available keys from all providers for governance selection
|
||||
getAllKeys: builder.query<DBKey[], void>({
|
||||
query: () => "/keys",
|
||||
providesTags: ["DBKeys"],
|
||||
}),
|
||||
|
||||
// Get models with optional filtering
|
||||
getModels: builder.query<ListModelsResponse, GetModelsRequest>({
|
||||
query: ({ query, provider, keys, vks, limit, unfiltered }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (query) params.append("query", query);
|
||||
if (provider) params.append("provider", provider);
|
||||
if (keys && keys.length > 0) params.append("keys", keys.join(","));
|
||||
if (vks && vks.length > 0) params.append("vks", vks.join(","));
|
||||
if (limit !== undefined) params.append("limit", limit.toString());
|
||||
if (unfiltered !== undefined) params.append("unfiltered", unfiltered.toString());
|
||||
return `/models?${params.toString()}`;
|
||||
},
|
||||
providesTags: ["Models"],
|
||||
}),
|
||||
|
||||
// Get distinct base model names from the catalog
|
||||
getBaseModels: builder.query<ListBaseModelsResponse, GetBaseModelsRequest>({
|
||||
query: ({ query, limit }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (query) params.append("query", query);
|
||||
if (limit !== undefined) params.append("limit", limit.toString());
|
||||
return `/models/base?${params.toString()}`;
|
||||
},
|
||||
providesTags: ["BaseModels"],
|
||||
}),
|
||||
|
||||
// Get model parameters (parameters, capabilities) from local API
|
||||
// Falls back to default parameters if the API returns an error (e.g. model not found)
|
||||
getModelParameters: builder.query<ModelDatasheetResponse, string>({
|
||||
queryFn: async (model, _queryApi, _extraOptions, baseQuery) => {
|
||||
const result = await baseQuery(`/models/parameters?model=${encodeURIComponent(model)}`);
|
||||
if (result.error) {
|
||||
// If the model is not found, return the default parameters
|
||||
if ((result.error as any)?.status === 404) {
|
||||
return { data: DEFAULT_MODEL_PARAMETERS };
|
||||
}
|
||||
return { error: result.error };
|
||||
}
|
||||
return { data: result.data as ModelDatasheetResponse };
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetProvidersQuery,
|
||||
useGetProviderQuery,
|
||||
useGetProviderKeysQuery,
|
||||
useGetProviderKeyQuery,
|
||||
useCreateProviderMutation,
|
||||
useUpdateProviderMutation,
|
||||
useCreateProviderKeyMutation,
|
||||
useUpdateProviderKeyMutation,
|
||||
useDeleteProviderKeyMutation,
|
||||
useDeleteProviderMutation,
|
||||
useGetAllKeysQuery,
|
||||
useGetModelsQuery,
|
||||
useGetBaseModelsQuery,
|
||||
useLazyGetProvidersQuery,
|
||||
useLazyGetProviderQuery,
|
||||
useLazyGetProviderKeysQuery,
|
||||
useLazyGetProviderKeyQuery,
|
||||
useLazyGetAllKeysQuery,
|
||||
useLazyGetModelsQuery,
|
||||
useLazyGetBaseModelsQuery,
|
||||
useGetModelParametersQuery,
|
||||
useLazyGetModelParametersQuery,
|
||||
} = providersApi;
|
||||
136
ui/lib/store/apis/routingRulesApi.ts
Normal file
136
ui/lib/store/apis/routingRulesApi.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Routing Rules RTK Query API
|
||||
* Handles all API communication for routing rules CRUD operations
|
||||
*/
|
||||
|
||||
import {
|
||||
RoutingRule,
|
||||
GetRoutingRulesResponse,
|
||||
GetRoutingRulesParams,
|
||||
CreateRoutingRuleRequest,
|
||||
UpdateRoutingRuleRequest,
|
||||
} from "@/lib/types/routingRules";
|
||||
import { baseApi } from "./baseApi";
|
||||
|
||||
export const routingRulesApi = baseApi.injectEndpoints({
|
||||
endpoints: (builder) => ({
|
||||
// Get routing rules with pagination
|
||||
getRoutingRules: builder.query<GetRoutingRulesResponse, GetRoutingRulesParams | void>({
|
||||
query: (params) => ({
|
||||
url: "/governance/routing-rules",
|
||||
params: {
|
||||
...(params?.limit && { limit: params.limit }),
|
||||
...(params?.offset !== undefined && { offset: params.offset }),
|
||||
...(params?.search && { search: params.search }),
|
||||
},
|
||||
}),
|
||||
providesTags: ["RoutingRules"],
|
||||
}),
|
||||
|
||||
// Get a single routing rule
|
||||
getRoutingRule: builder.query<RoutingRule, string>({
|
||||
query: (id) => ({
|
||||
url: `/governance/routing-rules/${id}`,
|
||||
method: "GET",
|
||||
}),
|
||||
transformResponse: (response: { rule: RoutingRule }) => response.rule,
|
||||
providesTags: (result, error, arg) => [{ type: "RoutingRules", id: arg }],
|
||||
}),
|
||||
|
||||
// Create a new routing rule
|
||||
createRoutingRule: builder.mutation<RoutingRule, CreateRoutingRuleRequest>({
|
||||
query: (body) => ({
|
||||
url: `/governance/routing-rules`,
|
||||
method: "POST",
|
||||
body,
|
||||
}),
|
||||
transformResponse: (response: { rule: RoutingRule }) => response.rule,
|
||||
async onQueryStarted(arg, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data: newRule } = await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getRoutingRules" || entry?.status !== "fulfilled") continue;
|
||||
const search = entry.originalArgs?.search as string | undefined;
|
||||
if (search && !newRule.name.toLowerCase().includes(search.toLowerCase())) continue;
|
||||
dispatch(
|
||||
routingRulesApi.util.updateQueryData("getRoutingRules", entry.originalArgs, (draft) => {
|
||||
if (!draft.rules) draft.rules = [];
|
||||
draft.rules.unshift(newRule);
|
||||
draft.count = (draft.count || 0) + 1;
|
||||
draft.total_count = (draft.total_count || 0) + 1;
|
||||
}),
|
||||
);
|
||||
}
|
||||
dispatch(routingRulesApi.util.updateQueryData("getRoutingRule", newRule.id, () => newRule));
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Update an existing routing rule
|
||||
updateRoutingRule: builder.mutation<RoutingRule, { id: string; data: UpdateRoutingRuleRequest }>({
|
||||
query: ({ id, data }) => ({
|
||||
url: `/governance/routing-rules/${id}`,
|
||||
method: "PUT",
|
||||
body: data,
|
||||
}),
|
||||
transformResponse: (response: { rule: RoutingRule }) => response.rule,
|
||||
async onQueryStarted({ id }, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
const { data: updatedRule } = await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getRoutingRules" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
routingRulesApi.util.updateQueryData("getRoutingRules", entry.originalArgs, (draft) => {
|
||||
if (!draft.rules) return;
|
||||
const index = draft.rules.findIndex((r) => r.id === id);
|
||||
if (index !== -1) {
|
||||
draft.rules[index] = updatedRule;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
dispatch(routingRulesApi.util.updateQueryData("getRoutingRule", updatedRule.id, () => updatedRule));
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete a routing rule
|
||||
deleteRoutingRule: builder.mutation<void, string>({
|
||||
query: (id) => ({
|
||||
url: `/governance/routing-rules/${id}`,
|
||||
method: "DELETE",
|
||||
}),
|
||||
async onQueryStarted(ruleId, { dispatch, getState, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
const queries = (getState() as any).api.queries;
|
||||
for (const entry of Object.values(queries) as any[]) {
|
||||
if (entry?.endpointName !== "getRoutingRules" || entry?.status !== "fulfilled") continue;
|
||||
dispatch(
|
||||
routingRulesApi.util.updateQueryData("getRoutingRules", entry.originalArgs, (draft) => {
|
||||
if (!draft.rules) return;
|
||||
const before = draft.rules.length;
|
||||
draft.rules = draft.rules.filter((r) => r.id !== ruleId);
|
||||
if (draft.rules.length < before) {
|
||||
draft.count = Math.max(0, (draft.count || 0) - 1);
|
||||
draft.total_count = Math.max(0, (draft.total_count || 0) - 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetRoutingRulesQuery,
|
||||
useGetRoutingRuleQuery,
|
||||
useCreateRoutingRuleMutation,
|
||||
useUpdateRoutingRuleMutation,
|
||||
useDeleteRoutingRuleMutation,
|
||||
useLazyGetRoutingRulesQuery,
|
||||
} = routingRulesApi;
|
||||
61
ui/lib/store/apis/sessionApi.ts
Normal file
61
ui/lib/store/apis/sessionApi.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { baseApi, clearAuthStorage } from "./baseApi";
|
||||
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IsAuthEnabledResponse {
|
||||
is_auth_enabled: boolean;
|
||||
has_valid_token: boolean;
|
||||
}
|
||||
|
||||
export interface LogoutResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const sessionApi = baseApi.injectEndpoints({
|
||||
overrideExisting: false,
|
||||
endpoints: (builder) => ({
|
||||
// Check if auth is enabled
|
||||
isAuthEnabled: builder.query<IsAuthEnabledResponse, void>({
|
||||
query: () => ({
|
||||
url: "/session/is-auth-enabled",
|
||||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
// Login endpoint
|
||||
login: builder.mutation<LoginResponse, LoginRequest>({
|
||||
query: (credentials) => ({
|
||||
url: "/session/login",
|
||||
method: "POST",
|
||||
body: credentials,
|
||||
}),
|
||||
invalidatesTags: [],
|
||||
}),
|
||||
|
||||
// Logout endpoint
|
||||
logout: builder.mutation<LogoutResponse, void>({
|
||||
query: () => ({
|
||||
url: "/session/logout",
|
||||
method: "POST",
|
||||
}),
|
||||
// After logout, clear token and all cached data
|
||||
async onQueryStarted(arg, { queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled;
|
||||
} catch {
|
||||
} finally {
|
||||
clearAuthStorage();
|
||||
}
|
||||
},
|
||||
invalidatesTags: ["Config", "Providers", "Logs", "VirtualKeys", "Teams", "Customers", "Budgets", "RateLimits"],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useIsAuthEnabledQuery, useLoginMutation, useLogoutMutation } = sessionApi;
|
||||
6
ui/lib/store/hooks.ts
Normal file
6
ui/lib/store/hooks.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import type { RootState, AppDispatch } from "./store";
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||
14
ui/lib/store/index.ts
Normal file
14
ui/lib/store/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Store configuration
|
||||
export { store, type AppDispatch, type RootState } from "./store";
|
||||
|
||||
// Redux Provider
|
||||
export { ReduxProvider } from "./provider";
|
||||
|
||||
// Typed hooks
|
||||
export { useAppDispatch, useAppSelector } from "./hooks";
|
||||
|
||||
// App state slice
|
||||
export * from "./slices";
|
||||
|
||||
// APIs
|
||||
export * from "./apis";
|
||||
11
ui/lib/store/provider.tsx
Normal file
11
ui/lib/store/provider.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "./store";
|
||||
|
||||
interface ReduxProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ReduxProvider({ children }: ReduxProviderProps) {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
223
ui/lib/store/slices/appSlice.ts
Normal file
223
ui/lib/store/slices/appSlice.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
// Define the shape of our app state
|
||||
export interface AppState {
|
||||
// UI State
|
||||
sidebarCollapsed: boolean;
|
||||
theme: "light" | "dark" | "system";
|
||||
|
||||
// Loading states for global operations
|
||||
isInitializing: boolean;
|
||||
isOnline: boolean;
|
||||
|
||||
// Current user/session info
|
||||
currentUser: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
} | null;
|
||||
|
||||
// Global notifications/toasts
|
||||
notifications: {
|
||||
id: string;
|
||||
type: "success" | "error" | "warning" | "info";
|
||||
title: string;
|
||||
message: string;
|
||||
timestamp: number;
|
||||
read: boolean;
|
||||
}[];
|
||||
|
||||
// Application settings
|
||||
settings: {
|
||||
autoRefresh: boolean;
|
||||
refreshInterval: number; // in seconds
|
||||
maxLogEntries: number;
|
||||
defaultPageSize: number;
|
||||
};
|
||||
|
||||
// Global error state
|
||||
globalError: {
|
||||
message: string;
|
||||
code?: string;
|
||||
timestamp: number;
|
||||
} | null;
|
||||
|
||||
// Selected items
|
||||
selectedItems: {
|
||||
providers: string[];
|
||||
virtualKeys: string[];
|
||||
teams: string[];
|
||||
customers: string[];
|
||||
logs: string[];
|
||||
};
|
||||
|
||||
// Feature flags
|
||||
features: {
|
||||
enableMCP: boolean;
|
||||
enableCaching: boolean;
|
||||
enableLogging: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Define initial state
|
||||
const initialState: AppState = {
|
||||
sidebarCollapsed: false,
|
||||
theme: "system",
|
||||
isInitializing: true,
|
||||
isOnline: true,
|
||||
currentUser: null,
|
||||
notifications: [],
|
||||
settings: {
|
||||
autoRefresh: false,
|
||||
refreshInterval: 30,
|
||||
maxLogEntries: 1000,
|
||||
defaultPageSize: 50,
|
||||
},
|
||||
globalError: null,
|
||||
features: {
|
||||
enableMCP: false,
|
||||
enableCaching: false,
|
||||
enableLogging: false,
|
||||
},
|
||||
selectedItems: {
|
||||
providers: [],
|
||||
virtualKeys: [],
|
||||
teams: [],
|
||||
customers: [],
|
||||
logs: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Create the slice
|
||||
const appSlice = createSlice({
|
||||
name: "app",
|
||||
initialState,
|
||||
reducers: {
|
||||
// UI Actions
|
||||
toggleSidebar: (state) => {
|
||||
state.sidebarCollapsed = !state.sidebarCollapsed;
|
||||
},
|
||||
|
||||
setSidebarCollapsed: (state, action: PayloadAction<boolean>) => {
|
||||
state.sidebarCollapsed = action.payload;
|
||||
},
|
||||
|
||||
setTheme: (state, action: PayloadAction<"light" | "dark" | "system">) => {
|
||||
state.theme = action.payload;
|
||||
},
|
||||
|
||||
// App State Actions
|
||||
setInitializing: (state, action: PayloadAction<boolean>) => {
|
||||
state.isInitializing = action.payload;
|
||||
},
|
||||
|
||||
setOnlineStatus: (state, action: PayloadAction<boolean>) => {
|
||||
state.isOnline = action.payload;
|
||||
},
|
||||
|
||||
// Notification Actions
|
||||
addNotification: (state, action: PayloadAction<Omit<AppState["notifications"][0], "id" | "timestamp" | "read">>) => {
|
||||
const notification = {
|
||||
...action.payload,
|
||||
id: Date.now().toString(),
|
||||
timestamp: Date.now(),
|
||||
read: false,
|
||||
};
|
||||
state.notifications.unshift(notification);
|
||||
|
||||
// Keep only last 50 notifications
|
||||
if (state.notifications.length > 50) {
|
||||
state.notifications = state.notifications.slice(0, 50);
|
||||
}
|
||||
},
|
||||
|
||||
markNotificationRead: (state, action: PayloadAction<string>) => {
|
||||
const notification = state.notifications.find((n) => n.id === action.payload);
|
||||
if (notification) {
|
||||
notification.read = true;
|
||||
}
|
||||
},
|
||||
|
||||
removeNotification: (state, action: PayloadAction<string>) => {
|
||||
state.notifications = state.notifications.filter((n) => n.id !== action.payload);
|
||||
},
|
||||
|
||||
clearAllNotifications: (state) => {
|
||||
state.notifications = [];
|
||||
},
|
||||
|
||||
markAllNotificationsRead: (state) => {
|
||||
state.notifications.forEach((notification) => {
|
||||
notification.read = true;
|
||||
});
|
||||
},
|
||||
|
||||
// Settings Actions
|
||||
updateSettings: (state, action: PayloadAction<Partial<AppState["settings"]>>) => {
|
||||
state.settings = { ...state.settings, ...action.payload };
|
||||
},
|
||||
|
||||
// Error Actions
|
||||
setGlobalError: (state, action: PayloadAction<AppState["globalError"]>) => {
|
||||
state.globalError = action.payload;
|
||||
},
|
||||
|
||||
clearGlobalError: (state) => {
|
||||
state.globalError = null;
|
||||
},
|
||||
|
||||
// Feature Flags Actions
|
||||
updateFeatures: (state, action: PayloadAction<Partial<AppState["features"]>>) => {
|
||||
state.features = { ...state.features, ...action.payload };
|
||||
},
|
||||
// Reset app state (useful for logout)
|
||||
resetAppState: () => initialState,
|
||||
},
|
||||
});
|
||||
|
||||
// Export actions
|
||||
export const {
|
||||
// UI Actions
|
||||
toggleSidebar,
|
||||
setSidebarCollapsed,
|
||||
setTheme,
|
||||
|
||||
// App State Actions
|
||||
setInitializing,
|
||||
setOnlineStatus,
|
||||
|
||||
// Notification Actions
|
||||
addNotification,
|
||||
markNotificationRead,
|
||||
removeNotification,
|
||||
clearAllNotifications,
|
||||
markAllNotificationsRead,
|
||||
|
||||
// Settings Actions
|
||||
updateSettings,
|
||||
|
||||
// Error Actions
|
||||
setGlobalError,
|
||||
clearGlobalError,
|
||||
|
||||
// Feature Flags Actions
|
||||
updateFeatures,
|
||||
|
||||
// Reset
|
||||
resetAppState,
|
||||
} = appSlice.actions;
|
||||
|
||||
// Export reducer
|
||||
export default appSlice.reducer;
|
||||
|
||||
// Selectors
|
||||
export const selectSidebarCollapsed = (state: { app: AppState }) => state.app.sidebarCollapsed;
|
||||
export const selectTheme = (state: { app: AppState }) => state.app.theme;
|
||||
export const selectIsInitializing = (state: { app: AppState }) => state.app.isInitializing;
|
||||
export const selectIsOnline = (state: { app: AppState }) => state.app.isOnline;
|
||||
export const selectCurrentUser = (state: { app: AppState }) => state.app.currentUser;
|
||||
export const selectNotifications = (state: { app: AppState }) => state.app.notifications;
|
||||
export const selectUnreadNotificationsCount = (state: { app: AppState }) => state.app.notifications.filter((n) => !n.read).length;
|
||||
export const selectSettings = (state: { app: AppState }) => state.app.settings;
|
||||
export const selectGlobalError = (state: { app: AppState }) => state.app.globalError;
|
||||
export const selectFeatures = (state: { app: AppState }) => state.app.features;
|
||||
14
ui/lib/store/slices/index.ts
Normal file
14
ui/lib/store/slices/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// App slice exports
|
||||
export * from "./appSlice";
|
||||
export { default as appReducer } from "./appSlice";
|
||||
|
||||
// Provider slice exports
|
||||
export * from "./providerSlice";
|
||||
export { default as providerReducer } from "./providerSlice";
|
||||
|
||||
// Plugin slice exports
|
||||
export * from "./pluginSlice";
|
||||
export { default as pluginReducer } from "./pluginSlice";
|
||||
|
||||
// Enterprise slice exports
|
||||
export * from "@enterprise/lib/store/slices";
|
||||
70
ui/lib/store/slices/pluginSlice.ts
Normal file
70
ui/lib/store/slices/pluginSlice.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Plugin } from "@/lib/types/plugins";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { pluginsApi } from "../apis";
|
||||
|
||||
interface PluginState {
|
||||
selectedPlugin?: Plugin;
|
||||
isDirty: boolean;
|
||||
}
|
||||
|
||||
const initialState: PluginState = {
|
||||
selectedPlugin: undefined,
|
||||
isDirty: false,
|
||||
};
|
||||
|
||||
const pluginSlice = createSlice({
|
||||
name: "plugin",
|
||||
initialState,
|
||||
reducers: {
|
||||
setPluginFormDirtyState: (state, action: PayloadAction<boolean>) => {
|
||||
state.isDirty = action.payload;
|
||||
},
|
||||
setSelectedPlugin: (state, action: PayloadAction<Plugin | undefined>) => {
|
||||
state.selectedPlugin = action.payload;
|
||||
},
|
||||
},
|
||||
|
||||
extraReducers: (builder) => {
|
||||
// Listen to getPlugins fulfilled to update selected plugin if it has changed
|
||||
builder.addMatcher(pluginsApi.endpoints.getPlugins.matchFulfilled, (state, action) => {
|
||||
const updatedPlugins = action.payload;
|
||||
|
||||
// If we have a selected plugin, check if it has been updated
|
||||
if (state.selectedPlugin && updatedPlugins) {
|
||||
const updatedSelectedPlugin = updatedPlugins.find((plugin) => plugin.name === state.selectedPlugin!.name);
|
||||
// If the selected plugin exists in the updated list, update it
|
||||
if (updatedSelectedPlugin) {
|
||||
state.selectedPlugin = updatedSelectedPlugin;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to updatePlugin fulfilled to update selected plugin if it's the same one
|
||||
builder.addMatcher(pluginsApi.endpoints.updatePlugin.matchFulfilled, (state, action) => {
|
||||
const updatedPluginName = action.meta.arg.originalArgs.name;
|
||||
// If the updated plugin is the currently selected one, update it
|
||||
if (state.selectedPlugin && updatedPluginName === state.selectedPlugin.name) {
|
||||
// Update the selected plugin with the new data
|
||||
const updatedPlugin = action.payload;
|
||||
state.selectedPlugin = updatedPlugin;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to deletePlugin fulfilled to remove the plugin from the list
|
||||
builder.addMatcher(pluginsApi.endpoints.deletePlugin.matchFulfilled, (state, action) => {
|
||||
const deletedPluginName = action.meta.arg.originalArgs;
|
||||
// If the deleted plugin was selected, clear the selection
|
||||
if (state.selectedPlugin && state.selectedPlugin.name === deletedPluginName) {
|
||||
state.selectedPlugin = undefined;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setPluginFormDirtyState, setSelectedPlugin } = pluginSlice.actions;
|
||||
|
||||
export default pluginSlice.reducer;
|
||||
|
||||
// Selectors
|
||||
export const selectSelectedPlugin = (state: { plugin: PluginState }) => state.plugin.selectedPlugin;
|
||||
export const selectPluginFormIsDirty = (state: { plugin: PluginState }) => state.plugin.isDirty;
|
||||
80
ui/lib/store/slices/providerSlice.ts
Normal file
80
ui/lib/store/slices/providerSlice.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ModelProvider } from "@/lib/types/config";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { providersApi } from "../apis";
|
||||
|
||||
interface ProviderState {
|
||||
selectedProvider: ModelProvider | null;
|
||||
isConfigureDialogOpen: boolean;
|
||||
providers: ModelProvider[];
|
||||
isDirty: boolean;
|
||||
}
|
||||
|
||||
const initialState: ProviderState = {
|
||||
selectedProvider: null,
|
||||
isConfigureDialogOpen: false,
|
||||
providers: [],
|
||||
isDirty: false,
|
||||
};
|
||||
|
||||
const providerSlice = createSlice({
|
||||
name: "provider",
|
||||
initialState,
|
||||
reducers: {
|
||||
setProviderFormDirtyState: (state, action: PayloadAction<boolean>) => {
|
||||
state.isDirty = action.payload;
|
||||
},
|
||||
setSelectedProvider: (state, action: PayloadAction<ModelProvider | null>) => {
|
||||
state.selectedProvider = action.payload;
|
||||
},
|
||||
setIsConfigureDialogOpen: (state, action: PayloadAction<boolean>) => {
|
||||
state.isConfigureDialogOpen = action.payload;
|
||||
},
|
||||
setProviders: (state, action: PayloadAction<ModelProvider[]>) => {
|
||||
state.providers = action.payload;
|
||||
},
|
||||
openConfigureDialog: (state, action: PayloadAction<ModelProvider | null>) => {
|
||||
state.selectedProvider = action.payload;
|
||||
state.isConfigureDialogOpen = true;
|
||||
},
|
||||
closeConfigureDialog: (state) => {
|
||||
state.selectedProvider = null;
|
||||
state.isConfigureDialogOpen = false;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Listen to getProviders fulfilled to update selected provider if it has changed
|
||||
builder.addMatcher(providersApi.endpoints.getProviders.matchFulfilled, (state, action) => {
|
||||
const updatedProviders = action.payload;
|
||||
|
||||
// If we have a selected provider, check if it has been updated
|
||||
if (state.selectedProvider && updatedProviders) {
|
||||
const updatedSelectedProvider = updatedProviders.find((provider) => provider.name === state.selectedProvider!.name);
|
||||
// If the selected provider exists in the updated list, update it
|
||||
if (updatedSelectedProvider) {
|
||||
// Check if the provider has actually changed
|
||||
state.selectedProvider = updatedSelectedProvider;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to updateProvider fulfilled to update selected provider if it's the same one
|
||||
builder.addMatcher(providersApi.endpoints.updateProvider.matchFulfilled, (state, action) => {
|
||||
const updatedProvider = action.payload;
|
||||
// If the updated provider is the currently selected one, update it
|
||||
if (state.selectedProvider && updatedProvider.name === state.selectedProvider.name) {
|
||||
state.selectedProvider = updatedProvider;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setProviderFormDirtyState,
|
||||
setSelectedProvider,
|
||||
setIsConfigureDialogOpen,
|
||||
setProviders,
|
||||
openConfigureDialog,
|
||||
closeConfigureDialog,
|
||||
} = providerSlice.actions;
|
||||
|
||||
export default providerSlice.reducer;
|
||||
46
ui/lib/store/store.ts
Normal file
46
ui/lib/store/store.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { baseApi } from "./apis/baseApi";
|
||||
import { appReducer, pluginReducer, providerReducer } from "./slices";
|
||||
import { reducers as enterpriseReducers, type EnterpriseState } from "@enterprise/lib/store/slices";
|
||||
// Importing enterprise APIs triggers their self-injection into baseApi
|
||||
import "@enterprise/lib/store/apis";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
// RTK Query API
|
||||
[baseApi.reducerPath]: baseApi.reducer,
|
||||
// App state slice
|
||||
app: appReducer,
|
||||
// Provider state slice
|
||||
provider: providerReducer,
|
||||
// Plugin state slice
|
||||
plugin: pluginReducer,
|
||||
// Enterprise reducers (if available)
|
||||
...enterpriseReducers,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
// Ignore these action types for RTK Query
|
||||
ignoredActions: [
|
||||
"persist/PERSIST",
|
||||
"persist/REHYDRATE",
|
||||
"api/executeQuery/pending",
|
||||
"api/executeQuery/fulfilled",
|
||||
"api/executeQuery/rejected",
|
||||
"api/executeMutation/pending",
|
||||
"api/executeMutation/fulfilled",
|
||||
"api/executeMutation/rejected",
|
||||
],
|
||||
// Ignore these field paths in all actions
|
||||
ignoredActionsPaths: ["meta.arg", "payload.timestamp"],
|
||||
// Ignore these paths in the state
|
||||
ignoredPaths: ["api.queries", "api.mutations"],
|
||||
},
|
||||
}).concat(baseApi.middleware),
|
||||
devTools: process.env.NODE_ENV !== "production",
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState> & EnterpriseState;
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
Reference in New Issue
Block a user