Merge remote-tracking branch 'upstream/main' into feat/356-usage-record-columns

This commit is contained in:
RWDai
2026-05-18 17:49:35 +08:00
187 changed files with 15067 additions and 1394 deletions

View File

@@ -0,0 +1,85 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AggregateImportRequest, ConfigImportRequest, UsersImportRequest } from '@/api/admin'
const { postMock } = vi.hoisted(() => ({
postMock: vi.fn(),
}))
vi.mock('@/api/client', () => ({
default: {
post: postMock,
},
}))
import { adminApi } from '@/api/admin'
const SYSTEM_DATA_IMPORT_TIMEOUT_MS = 10 * 60 * 1000
describe('adminApi system data import timeouts', () => {
beforeEach(() => {
postMock.mockReset()
postMock.mockResolvedValue({ data: {} })
})
it('uses a long timeout for config imports', async () => {
const payload = {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
global_models: [],
providers: [],
merge_mode: 'skip',
} satisfies ConfigImportRequest
await adminApi.importConfig(payload)
expect(postMock).toHaveBeenCalledWith(
'/api/admin/system/config/import',
payload,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
})
it('uses a long timeout for user imports', async () => {
const payload = {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
users: [],
merge_mode: 'skip',
} satisfies UsersImportRequest
await adminApi.importUsers(payload)
expect(postMock).toHaveBeenCalledWith(
'/api/admin/system/users/import',
payload,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
})
it('uses a long timeout for aggregate imports', async () => {
const payload = {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
config_data: {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
global_models: [],
providers: [],
},
user_data: {
version: '1',
exported_at: '2026-01-01T00:00:00.000Z',
users: [],
},
merge_mode: 'skip',
} satisfies AggregateImportRequest
await adminApi.importAggregateData(payload)
expect(postMock).toHaveBeenCalledWith(
'/api/admin/system/data/import',
payload,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
})
})

View File

@@ -4,6 +4,8 @@ import { cachedRequest, buildCacheKey } from '@/utils/cache'
import type { BillingSummary } from './auth'
import type { ApiKeyInstallSession, InstallSessionTargetSystem, InstallTargetCli } from './me'
const SYSTEM_DATA_IMPORT_TIMEOUT_MS = 10 * 60 * 1000
function extractConflictPayload(error: unknown): ManualUsageCleanupConflict | null {
if (!axios.isAxiosError(error) || error.response?.status !== 409) {
return null
@@ -310,15 +312,35 @@ export interface ManualUsageCleanupSummary {
records_deleted: number
}
export interface ManualUsageCleanupResponse {
export type ManualUsageCleanupMode = 'policy' | 'older_than_days' | 'before_now'
export type ManualUsageCleanupTarget = 'detail_body' | 'compressed_body' | 'headers' | 'records'
export interface ManualUsageCleanupTargets {
detail_body: boolean
compressed_body: boolean
headers: boolean
records: boolean
expired_keys: boolean
}
export interface ManualUsageCleanupRequest {
mode?: ManualUsageCleanupMode
older_than_days?: number
targets?: ManualUsageCleanupTarget[]
}
export interface ManualUsageCleanupTaskResponse {
message: string
mode: ManualUsageCleanupMode
requested_older_than_days: number | null
summary: ManualUsageCleanupSummary
total_affected: number
targets: ManualUsageCleanupTargets
task: CleanupRunRecord
}
export interface ManualUsageCleanupPreview {
mode: ManualUsageCleanupMode
requested_older_than_days: number | null
targets: ManualUsageCleanupTargets
effective_cutoffs: {
detail: string
compressed: string
@@ -834,7 +856,8 @@ export const adminApi = {
async importConfig(data: ConfigImportRequest): Promise<ConfigImportResponse> {
const response = await apiClient.post<ConfigImportResponse>(
'/api/admin/system/config/import',
data
data,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
return response.data
},
@@ -849,7 +872,8 @@ export const adminApi = {
async importUsers(data: UsersImportRequest): Promise<UsersImportResponse> {
const response = await apiClient.post<UsersImportResponse>(
'/api/admin/system/users/import',
data
data,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
return response.data
},
@@ -864,7 +888,8 @@ export const adminApi = {
async importAggregateData(data: AggregateImportRequest): Promise<AggregateImportResponse> {
const response = await apiClient.post<AggregateImportResponse>(
'/api/admin/system/data/import',
data
data,
{ timeout: SYSTEM_DATA_IMPORT_TIMEOUT_MS }
)
return response.data
},
@@ -1218,14 +1243,20 @@ export const adminApi = {
},
async runManualUsageCleanup(
params: { older_than_days?: number } = {}
): Promise<ManualUsageCleanupResponse | ManualUsageCleanupConflict> {
const body: Record<string, number> = {}
params: ManualUsageCleanupRequest = {}
): Promise<ManualUsageCleanupTaskResponse | ManualUsageCleanupConflict> {
const body: ManualUsageCleanupRequest = {}
if (params.mode) {
body.mode = params.mode
}
if (typeof params.older_than_days === 'number') {
body.older_than_days = params.older_than_days
}
if (params.targets?.length) {
body.targets = params.targets
}
try {
const response = await apiClient.post<ManualUsageCleanupResponse>(
const response = await apiClient.post<ManualUsageCleanupTaskResponse>(
'/api/admin/system/cleanup/usage/manual',
body
)
@@ -1240,12 +1271,18 @@ export const adminApi = {
},
async previewManualUsageCleanup(
params: { older_than_days?: number } = {}
params: ManualUsageCleanupRequest = {}
): Promise<ManualUsageCleanupPreview> {
const query: Record<string, number> = {}
const query: Record<string, string | number> = {}
if (params.mode) {
query.mode = params.mode
}
if (typeof params.older_than_days === 'number') {
query.older_than_days = params.older_than_days
}
if (params.targets?.length) {
query.targets = params.targets.join(',')
}
const response = await apiClient.get<ManualUsageCleanupPreview>(
'/api/admin/system/cleanup/usage/preview',
{ params: query }

View File

@@ -0,0 +1,186 @@
import client from './client'
import type { RoutingDecisionTrace } from '@/features/routing/utils/routingTrace'
import type {
RoutingGroupConfig,
RoutingRulePhase,
} from '@/features/routing/utils/routingPolicy'
export type RoutingBindingSubjectType = 'user' | 'api_key' | 'user_group'
export interface RoutingGroupRecord {
id: string
name: string
description?: string | null
enabled: boolean
is_system_default: boolean
config_json: RoutingGroupConfig
version: number
created_at: number
updated_at: number
published_at?: number | null
}
export interface RoutingGroupListResponse {
items: RoutingGroupRecord[]
total: number
}
export interface RoutingGroupVersionRecord {
id: string
group_id: string
version: number
config_json: RoutingGroupConfig
created_at: number
created_by?: string | null
}
export interface RoutingGroupVersionListResponse {
items: RoutingGroupVersionRecord[]
total: number
}
export interface RoutingGroupBindingRecord {
id: string
group_id: string
subject_type: RoutingBindingSubjectType
subject_id: string
is_default: boolean
allow_explicit_select: boolean
created_at: number
updated_at: number
}
export interface RoutingGroupBindingListResponse {
items: RoutingGroupBindingRecord[]
total: number
}
export interface RoutingGroupCreateRequest {
id?: string
name: string
description?: string | null
enabled?: boolean
is_system_default?: boolean
config_json?: RoutingGroupConfig
}
export interface RoutingGroupUpdateRequest {
name?: string
description?: string | null
enabled?: boolean
is_system_default?: boolean
config_json?: RoutingGroupConfig
version?: number
published_at?: number | null
}
export interface RoutingGroupBindingCreateRequest {
id?: string
group_id: string
subject_type: RoutingBindingSubjectType
subject_id: string
is_default?: boolean
allow_explicit_select?: boolean
}
export interface RoutingGroupBindingUpdateRequest {
group_id?: string
subject_type?: RoutingBindingSubjectType
subject_id?: string
is_default?: boolean
allow_explicit_select?: boolean
}
export interface RoutingDryRunRequest {
model: string
resolved_model?: string
api_format?: string
user_id?: string
api_key_id?: string
headers?: Record<string, string>
body?: unknown
phase?: RoutingRulePhase
}
export interface RoutingDryRunResponse {
group: RoutingGroupRecord
policy: unknown
trace_seed: RoutingDecisionTrace
patch_summary: unknown
mutated_body: unknown
mutated_headers: Record<string, string>
candidate_preview: unknown
}
export async function listRoutingGroups(): Promise<RoutingGroupListResponse> {
const response = await client.get<RoutingGroupListResponse>('/api/admin/routing/groups')
return response.data
}
export async function getRoutingGroup(groupId: string): Promise<RoutingGroupRecord> {
const response = await client.get<RoutingGroupRecord>(`/api/admin/routing/groups/${groupId}`)
return response.data
}
export async function createRoutingGroup(data: RoutingGroupCreateRequest): Promise<RoutingGroupRecord> {
const response = await client.post<RoutingGroupRecord>('/api/admin/routing/groups', data)
return response.data
}
export async function updateRoutingGroup(
groupId: string,
data: RoutingGroupUpdateRequest
): Promise<RoutingGroupRecord> {
const response = await client.patch<RoutingGroupRecord>(`/api/admin/routing/groups/${groupId}`, data)
return response.data
}
export async function deleteRoutingGroup(groupId: string): Promise<void> {
await client.delete(`/api/admin/routing/groups/${groupId}`)
}
export async function publishRoutingGroup(groupId: string): Promise<RoutingGroupRecord> {
const response = await client.post<RoutingGroupRecord>(`/api/admin/routing/groups/${groupId}/publish`)
return response.data
}
export async function listRoutingGroupVersions(groupId: string): Promise<RoutingGroupVersionListResponse> {
const response = await client.get<RoutingGroupVersionListResponse>(`/api/admin/routing/groups/${groupId}/versions`)
return response.data
}
export async function dryRunRoutingGroup(
groupId: string,
data: RoutingDryRunRequest
): Promise<RoutingDryRunResponse> {
const response = await client.post<RoutingDryRunResponse>(`/api/admin/routing/groups/${groupId}/dry-run`, data)
return response.data
}
export async function listRoutingGroupBindings(params?: {
group_id?: string
subject_type?: RoutingBindingSubjectType
subject_id?: string
}): Promise<RoutingGroupBindingListResponse> {
const response = await client.get<RoutingGroupBindingListResponse>('/api/admin/routing/bindings', { params })
return response.data
}
export async function createRoutingGroupBinding(
data: RoutingGroupBindingCreateRequest
): Promise<RoutingGroupBindingRecord> {
const response = await client.post<RoutingGroupBindingRecord>('/api/admin/routing/bindings', data)
return response.data
}
export async function updateRoutingGroupBinding(
bindingId: string,
data: RoutingGroupBindingUpdateRequest
): Promise<RoutingGroupBindingRecord> {
const response = await client.patch<RoutingGroupBindingRecord>(`/api/admin/routing/bindings/${bindingId}`, data)
return response.data
}
export async function deleteRoutingGroupBinding(bindingId: string): Promise<void> {
await client.delete(`/api/admin/routing/bindings/${bindingId}`)
}