mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
This commit introduces new admin dashboard pages for viewing system logs and usage statistics. It includes backend API endpoints for fetching log data, usage metrics, error details, and revenue by model. The frontend has been updated with new UI components and pages to display this information. Additionally, important log messages used for analytics have been marked to prevent accidental modification. Co-authored-by: db2002dominic <db2002dominic@gmail.com>
976 lines
27 KiB
TypeScript
976 lines
27 KiB
TypeScript
import { apiClient } from '../client';
|
|
import { z } from 'zod';
|
|
|
|
export const ProviderTypeSchema = z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
default_base_url: z.string(),
|
|
fixed_base_url: z.boolean(),
|
|
platform_url: z.string().nullable(),
|
|
});
|
|
|
|
export const UpstreamProviderSchema = z.object({
|
|
id: z.number(),
|
|
provider_type: z.string(),
|
|
base_url: z.string(),
|
|
api_key: z.string().optional(),
|
|
api_version: z.string().nullable().optional(),
|
|
enabled: z.boolean(),
|
|
provider_fee: z.number().optional(),
|
|
});
|
|
|
|
export const CreateUpstreamProviderSchema = z.object({
|
|
provider_type: z.string(),
|
|
base_url: z.string(),
|
|
api_key: z.string(),
|
|
api_version: z.string().nullable().optional(),
|
|
enabled: z.boolean().default(true),
|
|
provider_fee: z.number().optional(),
|
|
});
|
|
|
|
export const UpdateUpstreamProviderSchema = z.object({
|
|
provider_type: z.string().optional(),
|
|
base_url: z.string().optional(),
|
|
api_key: z.string().optional(),
|
|
api_version: z.string().nullable().optional(),
|
|
enabled: z.boolean().optional(),
|
|
provider_fee: z.number().optional(),
|
|
});
|
|
|
|
export const AdminModelPricingSchema = z.object({
|
|
prompt: z.number().optional(),
|
|
completion: z.number().optional(),
|
|
request: z.number().optional(),
|
|
image: z.number().optional(),
|
|
web_search: z.number().optional(),
|
|
internal_reasoning: z.number().optional(),
|
|
});
|
|
|
|
export const AdminModelArchitectureSchema = z.object({
|
|
modality: z.string().optional(),
|
|
input_modalities: z.array(z.string()).optional(),
|
|
output_modalities: z.array(z.string()).optional(),
|
|
tokenizer: z.string().optional(),
|
|
instruct_type: z.string().nullable().optional(),
|
|
});
|
|
|
|
export const AdminModelSchema = z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
description: z.string(),
|
|
created: z.number(),
|
|
context_length: z.number(),
|
|
architecture: AdminModelArchitectureSchema.or(z.record(z.any())),
|
|
pricing: AdminModelPricingSchema.or(z.record(z.any())),
|
|
per_request_limits: z.record(z.any()).nullable().optional(),
|
|
top_provider: z.record(z.any()).nullable().optional(),
|
|
upstream_provider_id: z.number().nullable().optional(),
|
|
enabled: z.boolean().default(true),
|
|
});
|
|
|
|
export const ProviderModelsSchema = z.object({
|
|
provider: z.object({
|
|
id: z.number(),
|
|
provider_type: z.string(),
|
|
base_url: z.string(),
|
|
}),
|
|
db_models: z.array(AdminModelSchema),
|
|
remote_models: z.array(AdminModelSchema),
|
|
});
|
|
|
|
export type ProviderType = z.infer<typeof ProviderTypeSchema>;
|
|
export type UpstreamProvider = z.infer<typeof UpstreamProviderSchema>;
|
|
export type CreateUpstreamProvider = z.infer<
|
|
typeof CreateUpstreamProviderSchema
|
|
>;
|
|
export type UpdateUpstreamProvider = z.infer<
|
|
typeof UpdateUpstreamProviderSchema
|
|
>;
|
|
export type AdminModel = z.infer<typeof AdminModelSchema>;
|
|
export type AdminModelPricing = z.infer<typeof AdminModelPricingSchema>;
|
|
export type AdminModelArchitecture = z.infer<
|
|
typeof AdminModelArchitectureSchema
|
|
>;
|
|
export type ProviderModels = z.infer<typeof ProviderModelsSchema>;
|
|
|
|
export interface AdminModelAsModel {
|
|
id: string;
|
|
name: string;
|
|
full_name: string;
|
|
description?: string;
|
|
modelType: string;
|
|
isEnabled: boolean;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
provider: string;
|
|
url: string;
|
|
api_key?: string;
|
|
input_cost: number;
|
|
output_cost: number;
|
|
min_cost_per_request: number;
|
|
min_cash_per_request: number;
|
|
contextLength?: number;
|
|
apiKeyRequired: boolean;
|
|
provider_id?: string;
|
|
is_free: boolean;
|
|
soft_deleted?: boolean;
|
|
has_own_api_key: boolean;
|
|
api_key_type: string;
|
|
}
|
|
|
|
export interface AdminModelGroup {
|
|
id: string;
|
|
provider: string;
|
|
group_api_key?: string;
|
|
group_url?: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export class AdminService {
|
|
static convertPricingToPerMillionTokens(
|
|
pricing: Record<string, unknown>
|
|
): Record<string, unknown> {
|
|
if (!pricing) return pricing;
|
|
const result = { ...pricing };
|
|
if (typeof result.prompt === 'number') {
|
|
result.prompt = result.prompt * 1000000;
|
|
}
|
|
if (typeof result.completion === 'number') {
|
|
result.completion = result.completion * 1000000;
|
|
}
|
|
if (typeof result.request === 'number') {
|
|
result.request = result.request * 1000000;
|
|
}
|
|
if (typeof result.image === 'number') {
|
|
result.image = result.image * 1000000;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static convertPricingToPerToken(
|
|
pricing: Record<string, unknown>
|
|
): Record<string, unknown> {
|
|
if (!pricing) return pricing;
|
|
const result = { ...pricing };
|
|
if (typeof result.prompt === 'number') {
|
|
result.prompt = result.prompt / 1000000;
|
|
}
|
|
if (typeof result.completion === 'number') {
|
|
result.completion = result.completion / 1000000;
|
|
}
|
|
if (typeof result.request === 'number') {
|
|
result.request = result.request / 1000000;
|
|
}
|
|
if (typeof result.image === 'number') {
|
|
result.image = result.image / 1000000;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static transformAdminModelToModel(
|
|
adminModel: AdminModel,
|
|
providerName?: string
|
|
): AdminModelAsModel {
|
|
const pricing = this.convertPricingToPerMillionTokens(adminModel.pricing);
|
|
const inputCost = (pricing?.prompt as number) || 0;
|
|
const outputCost = (pricing?.completion as number) || 0;
|
|
const requestCost = (pricing?.request as number) || 0;
|
|
|
|
return {
|
|
id: adminModel.id,
|
|
name: adminModel.name,
|
|
full_name: adminModel.name,
|
|
description: adminModel.description,
|
|
modelType:
|
|
((adminModel.architecture as Record<string, unknown>)
|
|
?.modality as string) || 'text',
|
|
isEnabled: adminModel.enabled,
|
|
createdAt: new Date(adminModel.created * 1000).toISOString(),
|
|
updatedAt: new Date(adminModel.created * 1000).toISOString(),
|
|
provider: providerName || 'Unknown',
|
|
url: '',
|
|
api_key: undefined,
|
|
input_cost: inputCost,
|
|
output_cost: outputCost,
|
|
min_cost_per_request: requestCost,
|
|
min_cash_per_request: 0,
|
|
contextLength: adminModel.context_length,
|
|
apiKeyRequired: true,
|
|
provider_id: adminModel.upstream_provider_id?.toString(),
|
|
is_free: inputCost === 0 && outputCost === 0,
|
|
soft_deleted: !adminModel.enabled,
|
|
has_own_api_key: false,
|
|
api_key_type: 'group',
|
|
};
|
|
}
|
|
|
|
static transformModelToAdminModel(model: AdminModelAsModel): AdminModel {
|
|
const pricing = this.convertPricingToPerToken({
|
|
prompt: model.input_cost,
|
|
completion: model.output_cost,
|
|
request: model.min_cost_per_request,
|
|
image: 0,
|
|
web_search: 0,
|
|
internal_reasoning: 0,
|
|
});
|
|
return {
|
|
id: model.id,
|
|
name: model.name,
|
|
description: model.description || '',
|
|
created: Math.floor(new Date(model.createdAt).getTime() / 1000),
|
|
context_length: model.contextLength || 0,
|
|
architecture: {
|
|
modality: model.modelType,
|
|
input_modalities: [model.modelType],
|
|
output_modalities: [model.modelType],
|
|
tokenizer: '',
|
|
instruct_type: null,
|
|
},
|
|
pricing,
|
|
per_request_limits: null,
|
|
top_provider: null,
|
|
upstream_provider_id: model.provider_id
|
|
? parseInt(model.provider_id)
|
|
: null,
|
|
enabled: model.isEnabled,
|
|
};
|
|
}
|
|
|
|
static async getProviderTypes(): Promise<ProviderType[]> {
|
|
return await apiClient.get<ProviderType[]>('/admin/api/provider-types');
|
|
}
|
|
|
|
static async getUpstreamProviders(): Promise<UpstreamProvider[]> {
|
|
return await apiClient.get<UpstreamProvider[]>(
|
|
'/admin/api/upstream-providers'
|
|
);
|
|
}
|
|
|
|
static async getUpstreamProvider(id: number): Promise<UpstreamProvider> {
|
|
return await apiClient.get<UpstreamProvider>(
|
|
`/admin/api/upstream-providers/${id}`
|
|
);
|
|
}
|
|
|
|
static async createUpstreamProvider(
|
|
data: CreateUpstreamProvider
|
|
): Promise<UpstreamProvider> {
|
|
return await apiClient.post<UpstreamProvider>(
|
|
'/admin/api/upstream-providers',
|
|
data
|
|
);
|
|
}
|
|
|
|
static async updateUpstreamProvider(
|
|
id: number,
|
|
data: UpdateUpstreamProvider
|
|
): Promise<UpstreamProvider> {
|
|
return await apiClient.patch<UpstreamProvider>(
|
|
`/admin/api/upstream-providers/${id}`,
|
|
data
|
|
);
|
|
}
|
|
|
|
static async deleteUpstreamProvider(
|
|
id: number
|
|
): Promise<{ ok: boolean; deleted_id: number }> {
|
|
return await apiClient.delete<{ ok: boolean; deleted_id: number }>(
|
|
`/admin/api/upstream-providers/${id}`
|
|
);
|
|
}
|
|
|
|
static async getProviderModels(providerId: number): Promise<ProviderModels> {
|
|
const data = await apiClient.get<ProviderModels>(
|
|
`/admin/api/upstream-providers/${providerId}/models`
|
|
);
|
|
return data;
|
|
}
|
|
|
|
static async createProviderModel(
|
|
providerId: number,
|
|
data: AdminModel
|
|
): Promise<AdminModel> {
|
|
const payload = {
|
|
...data,
|
|
pricing: this.convertPricingToPerToken(data.pricing),
|
|
};
|
|
console.log('Creating provider model - pricing conversion:', {
|
|
original: data.pricing,
|
|
converted: payload.pricing,
|
|
});
|
|
const model = await apiClient.post<AdminModel>(
|
|
`/admin/api/upstream-providers/${providerId}/models`,
|
|
payload
|
|
);
|
|
return {
|
|
...model,
|
|
pricing: this.convertPricingToPerMillionTokens(model.pricing),
|
|
};
|
|
}
|
|
|
|
static async getProviderModel(
|
|
providerId: number,
|
|
modelId: string
|
|
): Promise<AdminModel> {
|
|
const model = await apiClient.get<AdminModel>(
|
|
`/admin/api/upstream-providers/${providerId}/models/${encodeURIComponent(modelId)}`
|
|
);
|
|
return {
|
|
...model,
|
|
pricing: this.convertPricingToPerMillionTokens(model.pricing),
|
|
};
|
|
}
|
|
|
|
static async getModel(
|
|
modelId: string,
|
|
providerId: number | null = null
|
|
): Promise<AdminModel> {
|
|
const model = await apiClient.post<AdminModel>('/admin/api/models/get', {
|
|
model_id: modelId,
|
|
provider_id: providerId,
|
|
});
|
|
return {
|
|
...model,
|
|
pricing: this.convertPricingToPerMillionTokens(model.pricing),
|
|
};
|
|
}
|
|
|
|
static async updateProviderModel(
|
|
providerId: number,
|
|
modelId: string,
|
|
data: AdminModel
|
|
): Promise<AdminModel> {
|
|
const payload = {
|
|
...data,
|
|
pricing: this.convertPricingToPerToken(data.pricing),
|
|
};
|
|
console.log('Updating provider model - pricing conversion:', {
|
|
original: data.pricing,
|
|
converted: payload.pricing,
|
|
});
|
|
const model = await apiClient.patch<AdminModel>(
|
|
`/admin/api/upstream-providers/${providerId}/models/${encodeURIComponent(modelId)}`,
|
|
payload
|
|
);
|
|
return {
|
|
...model,
|
|
pricing: this.convertPricingToPerMillionTokens(model.pricing),
|
|
};
|
|
}
|
|
|
|
static async deleteProviderModel(
|
|
providerId: number,
|
|
modelId: string
|
|
): Promise<{ ok: boolean; deleted_id: string }> {
|
|
return await apiClient.delete<{ ok: boolean; deleted_id: string }>(
|
|
`/admin/api/upstream-providers/${providerId}/models/${encodeURIComponent(modelId)}`
|
|
);
|
|
}
|
|
|
|
static async deleteAllProviderModels(
|
|
providerId: number
|
|
): Promise<{ ok: boolean; deleted: number }> {
|
|
return await apiClient.delete<{ ok: boolean; deleted: number }>(
|
|
`/admin/api/upstream-providers/${providerId}/models`
|
|
);
|
|
}
|
|
|
|
static async getModelsWithProviders(): Promise<{
|
|
models: AdminModelAsModel[];
|
|
groups: AdminModelGroup[];
|
|
}> {
|
|
const providers = await this.getUpstreamProviders();
|
|
|
|
const groups: AdminModelGroup[] = providers.map((p) => ({
|
|
id: p.id.toString(),
|
|
provider: p.provider_type,
|
|
group_api_key: p.api_key,
|
|
group_url: p.base_url,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
}));
|
|
|
|
const allModels: AdminModelAsModel[] = [];
|
|
const seenModelIds = new Set<string>();
|
|
|
|
for (const provider of providers) {
|
|
try {
|
|
const providerModels = await this.getProviderModels(provider.id);
|
|
|
|
providerModels.db_models.forEach((dbModel) => {
|
|
seenModelIds.add(dbModel.id);
|
|
const modelWithProvider = {
|
|
...dbModel,
|
|
upstream_provider_id: provider.id,
|
|
};
|
|
allModels.push({
|
|
...this.transformAdminModelToModel(
|
|
modelWithProvider,
|
|
provider.provider_type
|
|
),
|
|
has_own_api_key: false,
|
|
api_key_type: 'group',
|
|
});
|
|
});
|
|
|
|
providerModels.remote_models.forEach((remoteModel) => {
|
|
if (!seenModelIds.has(remoteModel.id)) {
|
|
const modelWithProvider = {
|
|
...remoteModel,
|
|
upstream_provider_id: provider.id,
|
|
};
|
|
allModels.push({
|
|
...this.transformAdminModelToModel(
|
|
modelWithProvider,
|
|
provider.provider_type
|
|
),
|
|
has_own_api_key: false,
|
|
api_key_type: 'remote',
|
|
soft_deleted: false,
|
|
});
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to fetch models for provider ${provider.id}:`,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
return { models: allModels, groups };
|
|
}
|
|
|
|
static async createModelGroup(data: {
|
|
provider: string;
|
|
group_api_key?: string;
|
|
group_url?: string;
|
|
}): Promise<AdminModelGroup> {
|
|
const provider = await this.createUpstreamProvider({
|
|
provider_type: data.provider,
|
|
base_url: data.group_url || '',
|
|
api_key: data.group_api_key || '',
|
|
enabled: true,
|
|
});
|
|
return {
|
|
id: provider.id.toString(),
|
|
provider: provider.provider_type,
|
|
group_api_key: provider.api_key,
|
|
group_url: provider.base_url,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
static async updateModelGroup(
|
|
id: string,
|
|
data: { provider?: string; group_api_key?: string; group_url?: string }
|
|
): Promise<AdminModelGroup> {
|
|
const provider = await this.updateUpstreamProvider(parseInt(id), {
|
|
provider_type: data.provider,
|
|
base_url: data.group_url,
|
|
api_key: data.group_api_key,
|
|
});
|
|
return {
|
|
id: provider.id.toString(),
|
|
provider: provider.provider_type,
|
|
group_api_key: provider.api_key,
|
|
group_url: provider.base_url,
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
static async createModel(
|
|
data: Record<string, unknown>
|
|
): Promise<AdminModelAsModel> {
|
|
const providerId = data.provider_id
|
|
? parseInt(data.provider_id as string)
|
|
: null;
|
|
|
|
const pricing = {
|
|
prompt: (data.input_cost as number) / 1000000,
|
|
completion: (data.output_cost as number) / 1000000,
|
|
request: (data.min_cost_per_request as number) || 0,
|
|
image: 0,
|
|
web_search: 0,
|
|
internal_reasoning: 0,
|
|
};
|
|
|
|
const modelId = data.id as string;
|
|
|
|
const payload = {
|
|
model_id: modelId,
|
|
provider_id: providerId,
|
|
name: (data.name as string) || (data.full_name as string),
|
|
description: (data.description as string) || '',
|
|
created: Math.floor(Date.now() / 1000),
|
|
context_length: (data.contextLength as number) || 0,
|
|
architecture: {
|
|
modality: (data.modelType as string) || 'text',
|
|
input_modalities: [(data.modelType as string) || 'text'],
|
|
output_modalities: [(data.modelType as string) || 'text'],
|
|
tokenizer: '',
|
|
instruct_type: null,
|
|
},
|
|
pricing: this.convertPricingToPerToken(pricing),
|
|
per_request_limits: null,
|
|
top_provider: null,
|
|
enabled: data.isEnabled !== false,
|
|
};
|
|
|
|
const created = await apiClient.post<AdminModel>(
|
|
'/admin/api/models/create',
|
|
payload
|
|
);
|
|
|
|
return this.transformAdminModelToModel(
|
|
{
|
|
...created,
|
|
pricing: this.convertPricingToPerMillionTokens(created.pricing),
|
|
},
|
|
data.provider as string
|
|
);
|
|
}
|
|
|
|
static async updateModel(
|
|
modelId: string,
|
|
data: Record<string, unknown>
|
|
): Promise<AdminModelAsModel> {
|
|
const providerId = data.provider_id
|
|
? parseInt(data.provider_id as string)
|
|
: null;
|
|
|
|
const existingModel = await this.getModel(modelId, providerId);
|
|
|
|
const pricing = {
|
|
prompt: (data.input_cost as number) / 1000000,
|
|
completion: (data.output_cost as number) / 1000000,
|
|
request: (data.min_cost_per_request as number) || 0,
|
|
image: 0,
|
|
web_search: 0,
|
|
internal_reasoning: 0,
|
|
};
|
|
|
|
const payload: AdminModel & {
|
|
model_id: string;
|
|
provider_id: number | null;
|
|
} = {
|
|
...existingModel,
|
|
model_id: modelId,
|
|
provider_id: providerId,
|
|
pricing,
|
|
};
|
|
|
|
if (data.name) payload.name = data.name as string;
|
|
if (data.description) payload.description = data.description as string;
|
|
if (data.contextLength !== undefined)
|
|
payload.context_length = data.contextLength as number;
|
|
if (data.isEnabled !== undefined)
|
|
payload.enabled = data.isEnabled as boolean;
|
|
|
|
const updated = await apiClient.post<AdminModel>(
|
|
'/admin/api/models/update',
|
|
{
|
|
...payload,
|
|
pricing: this.convertPricingToPerToken(payload.pricing),
|
|
}
|
|
);
|
|
|
|
return this.transformAdminModelToModel(
|
|
{
|
|
...updated,
|
|
pricing: this.convertPricingToPerMillionTokens(updated.pricing),
|
|
},
|
|
data.provider as string
|
|
);
|
|
}
|
|
|
|
static async deleteModel(
|
|
modelId: string,
|
|
providerId?: string
|
|
): Promise<{ message: string }> {
|
|
const providerIdNum = providerId ? parseInt(providerId) : null;
|
|
await apiClient.post('/admin/api/models/delete', {
|
|
model_id: modelId,
|
|
provider_id: providerIdNum,
|
|
});
|
|
return { message: 'Model deleted successfully' };
|
|
}
|
|
|
|
static async softDeleteModel(
|
|
modelId: string,
|
|
providerId?: string
|
|
): Promise<{ message: string }> {
|
|
const providerIdNum = providerId ? parseInt(providerId) : null;
|
|
const model = await this.getModel(modelId, providerIdNum);
|
|
|
|
await apiClient.post('/admin/api/models/update', {
|
|
model_id: modelId,
|
|
provider_id: providerIdNum,
|
|
...model,
|
|
enabled: false,
|
|
pricing: this.convertPricingToPerToken(model.pricing),
|
|
});
|
|
|
|
return { message: 'Model soft deleted successfully' };
|
|
}
|
|
|
|
static async deleteModels(
|
|
modelIds: string[],
|
|
providerId?: string
|
|
): Promise<{ deleted_count: number; message: string }> {
|
|
if (!providerId) {
|
|
throw new Error('provider_id is required to delete models');
|
|
}
|
|
const providerIdNum = parseInt(providerId);
|
|
for (const id of modelIds) {
|
|
await this.deleteProviderModel(providerIdNum, id);
|
|
}
|
|
return {
|
|
deleted_count: modelIds.length,
|
|
message: 'Models deleted successfully',
|
|
};
|
|
}
|
|
|
|
static async softDeleteModels(
|
|
modelIds: string[],
|
|
providerId?: string
|
|
): Promise<{ deleted_count: number; message: string }> {
|
|
if (!providerId) {
|
|
throw new Error('provider_id is required to soft delete models');
|
|
}
|
|
const providerIdNum = parseInt(providerId);
|
|
for (const id of modelIds) {
|
|
const model = await this.getProviderModel(providerIdNum, id);
|
|
await this.updateProviderModel(providerIdNum, id, {
|
|
...model,
|
|
enabled: false,
|
|
});
|
|
}
|
|
return {
|
|
deleted_count: modelIds.length,
|
|
message: 'Models soft deleted successfully',
|
|
};
|
|
}
|
|
|
|
static async bulkUpdateModels(
|
|
modelIds: string[],
|
|
updates: { api_key?: string; url?: string },
|
|
providerId?: string
|
|
): Promise<{
|
|
updated_count: number;
|
|
total_count: number;
|
|
message: string;
|
|
errors: string[];
|
|
}> {
|
|
if (!providerId) {
|
|
throw new Error('provider_id is required for bulk updates');
|
|
}
|
|
|
|
const errors: string[] = [];
|
|
let updated_count = 0;
|
|
const providerIdNum = parseInt(providerId);
|
|
|
|
console.log('Bulk update not implemented, ignoring updates:', updates);
|
|
|
|
for (const id of modelIds) {
|
|
try {
|
|
const model = await this.getProviderModel(providerIdNum, id);
|
|
await this.updateProviderModel(providerIdNum, id, model);
|
|
updated_count++;
|
|
} catch (error: unknown) {
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : 'Unknown error';
|
|
errors.push(`Failed to update ${id}: ${errorMessage}`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
updated_count,
|
|
total_count: modelIds.length,
|
|
message: 'Bulk update completed',
|
|
errors,
|
|
};
|
|
}
|
|
|
|
static async deleteAllModels(): Promise<{
|
|
deleted_count: number;
|
|
message: string;
|
|
}> {
|
|
const providers = await this.getUpstreamProviders();
|
|
let totalDeleted = 0;
|
|
|
|
for (const provider of providers) {
|
|
const result = await this.deleteAllProviderModels(provider.id);
|
|
totalDeleted += result.deleted;
|
|
}
|
|
|
|
return {
|
|
deleted_count: totalDeleted,
|
|
message: 'All models deleted successfully',
|
|
};
|
|
}
|
|
|
|
static async deleteModelsByProvider(
|
|
providerId: string
|
|
): Promise<{ deleted_count: number; message: string }> {
|
|
const result = await this.deleteAllProviderModels(parseInt(providerId));
|
|
return {
|
|
deleted_count: result.deleted,
|
|
message: 'Provider models deleted successfully',
|
|
};
|
|
}
|
|
|
|
static async restoreModels(
|
|
modelIds: string[],
|
|
providerId?: string
|
|
): Promise<{ restored_count: number; message: string }> {
|
|
if (!providerId) {
|
|
throw new Error('provider_id is required to restore models');
|
|
}
|
|
const providerIdNum = parseInt(providerId);
|
|
for (const id of modelIds) {
|
|
const model = await this.getProviderModel(providerIdNum, id);
|
|
await this.updateProviderModel(providerIdNum, id, {
|
|
...model,
|
|
enabled: true,
|
|
});
|
|
}
|
|
return {
|
|
restored_count: modelIds.length,
|
|
message: 'Models restored successfully',
|
|
};
|
|
}
|
|
|
|
static async refreshAllModels(): Promise<{ message: string }> {
|
|
return { message: 'Refresh not implemented for admin API' };
|
|
}
|
|
|
|
static async refreshModels(data: {
|
|
provider_id: string;
|
|
}): Promise<{ message: string }> {
|
|
console.log(
|
|
'Refresh models not implemented for admin API, ignoring data:',
|
|
data
|
|
);
|
|
return { message: 'Refresh not implemented for admin API' };
|
|
}
|
|
|
|
static async getOpenRouterPresets(): Promise<AdminModel[]> {
|
|
const presets = await apiClient.get<AdminModel[]>(
|
|
'/admin/api/openrouter-presets'
|
|
);
|
|
return presets.map((m) => ({
|
|
...m,
|
|
pricing: this.convertPricingToPerMillionTokens(m.pricing),
|
|
}));
|
|
}
|
|
|
|
static async getSettings(): Promise<Record<string, unknown>> {
|
|
return await apiClient.get<Record<string, unknown>>('/admin/api/settings');
|
|
}
|
|
|
|
static async updateSettings(
|
|
settings: Record<string, unknown>
|
|
): Promise<Record<string, unknown>> {
|
|
return await apiClient.patch<Record<string, unknown>>(
|
|
'/admin/api/settings',
|
|
settings
|
|
);
|
|
}
|
|
|
|
static async login(password: string): Promise<{
|
|
ok: boolean;
|
|
token: string;
|
|
expires_in: number;
|
|
}> {
|
|
return await apiClient.post<{
|
|
ok: boolean;
|
|
token: string;
|
|
expires_in: number;
|
|
}>('/admin/api/login', { password });
|
|
}
|
|
|
|
static async logout(): Promise<{ ok: boolean }> {
|
|
return await apiClient.post<{ ok: boolean }>('/admin/api/logout', {});
|
|
}
|
|
|
|
static async getTemporaryBalances(): Promise<TemporaryBalance[]> {
|
|
return await apiClient.get<TemporaryBalance[]>(
|
|
'/admin/api/temporary-balances'
|
|
);
|
|
}
|
|
|
|
static async getLogs(params: {
|
|
date?: string;
|
|
level?: string;
|
|
requestId?: string;
|
|
search?: string;
|
|
limit?: number;
|
|
}): Promise<LogsResponse> {
|
|
const query: Record<string, string | number | undefined> = {
|
|
date: params.date,
|
|
level: params.level,
|
|
request_id: params.requestId,
|
|
search: params.search,
|
|
limit: params.limit,
|
|
};
|
|
Object.keys(query).forEach((key) => {
|
|
if (query[key] === undefined || query[key] === '') {
|
|
delete query[key];
|
|
}
|
|
});
|
|
return await apiClient.get<LogsResponse>('/admin/api/logs', query);
|
|
}
|
|
|
|
static async getLogDates(): Promise<string[]> {
|
|
const response = await apiClient.get<{ dates: string[] }>(
|
|
'/admin/api/logs/dates'
|
|
);
|
|
return response.dates;
|
|
}
|
|
|
|
static async getUsageMetrics(
|
|
interval: number = 15,
|
|
hours: number = 24
|
|
): Promise<UsageMetrics> {
|
|
return await apiClient.get<UsageMetrics>(
|
|
`/admin/api/usage/metrics?interval=${interval}&hours=${hours}`
|
|
);
|
|
}
|
|
|
|
static async getUsageSummary(hours: number = 24): Promise<UsageSummary> {
|
|
return await apiClient.get<UsageSummary>(
|
|
`/admin/api/usage/summary?hours=${hours}`
|
|
);
|
|
}
|
|
|
|
static async getErrorDetails(
|
|
hours: number = 24,
|
|
limit: number = 100
|
|
): Promise<ErrorDetails> {
|
|
return await apiClient.get<ErrorDetails>(
|
|
`/admin/api/usage/error-details?hours=${hours}&limit=${limit}`
|
|
);
|
|
}
|
|
|
|
static async getRevenueByModel(
|
|
hours: number = 24,
|
|
limit: number = 20
|
|
): Promise<RevenueByModel> {
|
|
return await apiClient.get<RevenueByModel>(
|
|
`/admin/api/usage/revenue-by-model?hours=${hours}&limit=${limit}`
|
|
);
|
|
}
|
|
}
|
|
|
|
export const TemporaryBalanceSchema = z.object({
|
|
hashed_key: z.string(),
|
|
balance: z.number(),
|
|
total_spent: z.number(),
|
|
total_requests: z.number(),
|
|
refund_address: z.string().nullable(),
|
|
key_expiry_time: z.number().nullable(),
|
|
});
|
|
|
|
export type TemporaryBalance = z.infer<typeof TemporaryBalanceSchema>;
|
|
|
|
export interface LogEntry {
|
|
asctime?: string;
|
|
name?: string;
|
|
levelname?: string;
|
|
message?: string;
|
|
pathname?: string;
|
|
lineno?: number;
|
|
version?: string;
|
|
request_id?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface LogsResponse {
|
|
logs: LogEntry[];
|
|
total: number;
|
|
date: string | null;
|
|
level: string | null;
|
|
request_id: string | null;
|
|
search: string | null;
|
|
limit: number;
|
|
}
|
|
|
|
export interface UsageMetricData {
|
|
timestamp: string;
|
|
total_requests: number;
|
|
successful_chat_completions: number;
|
|
failed_requests: number;
|
|
errors: number;
|
|
warnings: number;
|
|
payment_processed: number;
|
|
upstream_errors: number;
|
|
revenue_msats: number;
|
|
refunds_msats: number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export interface UsageMetrics {
|
|
metrics: UsageMetricData[];
|
|
interval_minutes: number;
|
|
hours_back: number;
|
|
total_buckets: number;
|
|
}
|
|
|
|
export interface UsageSummary {
|
|
total_entries: number;
|
|
total_requests: number;
|
|
successful_chat_completions: number;
|
|
failed_requests: number;
|
|
total_errors: number;
|
|
total_warnings: number;
|
|
payment_processed: number;
|
|
upstream_errors: number;
|
|
unique_models_count: number;
|
|
unique_models: string[];
|
|
error_types: Record<string, number>;
|
|
success_rate: number;
|
|
revenue_msats: number;
|
|
refunds_msats: number;
|
|
revenue_sats: number;
|
|
refunds_sats: number;
|
|
net_revenue_msats: number;
|
|
net_revenue_sats: number;
|
|
avg_revenue_per_request_msats: number;
|
|
refund_rate: number;
|
|
}
|
|
|
|
export interface ErrorDetail {
|
|
timestamp: string;
|
|
message: string;
|
|
error_type: string;
|
|
pathname: string;
|
|
lineno: number;
|
|
request_id: string;
|
|
}
|
|
|
|
export interface ErrorDetails {
|
|
errors: ErrorDetail[];
|
|
total_count: number;
|
|
}
|
|
|
|
export interface ModelRevenueData {
|
|
model: string;
|
|
revenue_sats: number;
|
|
refunds_sats: number;
|
|
net_revenue_sats: number;
|
|
requests: number;
|
|
successful: number;
|
|
failed: number;
|
|
avg_revenue_per_request: number;
|
|
}
|
|
|
|
export interface RevenueByModel {
|
|
models: ModelRevenueData[];
|
|
total_revenue_sats: number;
|
|
total_models: number;
|
|
}
|