mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
Merge remote-tracking branch 'upstream/main' into feat/356-usage-record-columns
This commit is contained in:
85
frontend/src/api/__tests__/admin-import-timeout.spec.ts
Normal file
85
frontend/src/api/__tests__/admin-import-timeout.spec.ts
Normal 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 }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
|
||||
186
frontend/src/api/routing-profiles.ts
Normal file
186
frontend/src/api/routing-profiles.ts
Normal 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}`)
|
||||
}
|
||||
Reference in New Issue
Block a user