first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View 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";
};

View 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
View 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

View 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;

View 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";

View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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
View 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>;
}

View 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;

View 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";

View 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;

View 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
View 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;