mirror of
https://github.com/YouXam/claude-code-usage-dashboard.git
synced 2026-02-04 15:10:16 +08:00
fix api format
This commit is contained in:
@@ -6,24 +6,16 @@ interface LoginResponse {
|
|||||||
|
|
||||||
interface ApiKeysResponse {
|
interface ApiKeysResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: Array<{
|
data: {
|
||||||
id: string;
|
items: ApiKeyListItem[];
|
||||||
name: string;
|
pagination: {
|
||||||
tags: string[];
|
page: number;
|
||||||
usage: {
|
pageSize: number;
|
||||||
total: {
|
total: number;
|
||||||
cost: number;
|
totalPages: number;
|
||||||
tokens: number;
|
|
||||||
inputTokens: number;
|
|
||||||
outputTokens: number;
|
|
||||||
cacheCreateTokens: number;
|
|
||||||
cacheReadTokens: number;
|
|
||||||
requests: number;
|
|
||||||
formattedCost: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
[key: string]: any;
|
availableTags: string[];
|
||||||
}>;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyIdResponse {
|
interface KeyIdResponse {
|
||||||
@@ -33,6 +25,48 @@ interface KeyIdResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiKeyListItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
apiKey?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiKeyUsageTotals {
|
||||||
|
cost: number;
|
||||||
|
tokens: number;
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
cacheCreateTokens: number;
|
||||||
|
cacheReadTokens: number;
|
||||||
|
requests: number;
|
||||||
|
formattedCost: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiKeyWithUsage extends ApiKeyListItem {
|
||||||
|
usage: {
|
||||||
|
total: ApiKeyUsageTotals;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchStatsResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: Record<string, ApiKeyBatchStats>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiKeyBatchStats {
|
||||||
|
requests: number;
|
||||||
|
tokens: number;
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
cacheCreateTokens: number;
|
||||||
|
cacheReadTokens: number;
|
||||||
|
cost: number;
|
||||||
|
formattedCost?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
export class ApiClient {
|
export class ApiClient {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private username: string;
|
private username: string;
|
||||||
@@ -103,31 +137,118 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentCosts(): Promise<ApiKeysResponse['data']> {
|
private async fetchApiKeysPage(page: number, pageSize: number): Promise<ApiKeysResponse['data']> {
|
||||||
await this.ensureValidToken();
|
await this.ensureValidToken();
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/admin/api-keys?timeRange=all`, {
|
const response = await fetch(
|
||||||
method: 'GET',
|
`${this.baseUrl}/admin/api-keys?page=${page}&pageSize=${pageSize}&searchMode=apiKey&sortBy=createdAt&sortOrder=desc&timeRange=all`,
|
||||||
headers: {
|
{
|
||||||
'Authorization': `Bearer ${this.token}`,
|
method: 'GET',
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
},
|
'Authorization': `Bearer ${this.token}`,
|
||||||
});
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch current costs: ${response.status} ${response.statusText}`);
|
throw new Error(`Failed to fetch api keys: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json() as ApiKeysResponse;
|
const data = await response.json() as ApiKeysResponse;
|
||||||
|
|
||||||
if (!data.success || !Array.isArray(data.data)) {
|
if (!data.success || !data.data?.items || !data.data?.pagination) {
|
||||||
throw new Error('Invalid response format from admin/api-keys');
|
throw new Error('Invalid response format from admin/api-keys');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.data.filter(user => !user.tags.includes("noshare"));
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKeyId(apiKey: string): Promise<string> {
|
private async fetchUsageBatch(keyIds: string[]): Promise<Record<string, ApiKeyBatchStats>> {
|
||||||
|
await this.ensureValidToken();
|
||||||
|
|
||||||
|
const response = await fetch(`${this.baseUrl}/admin/api-keys/batch-stats`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
keyIds,
|
||||||
|
timeRange: 'all',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch usage stats: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as BatchStatsResponse;
|
||||||
|
|
||||||
|
if (!data.success || !data.data) {
|
||||||
|
throw new Error('Invalid response format from admin/api-keys/batch-stats');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getUsageStats(keyIds: string[]): Promise<Map<string, ApiKeyBatchStats>> {
|
||||||
|
const usageMap = new Map<string, ApiKeyBatchStats>();
|
||||||
|
const batchSize = 10;
|
||||||
|
|
||||||
|
for (let i = 0; i < keyIds.length; i += batchSize) {
|
||||||
|
const batch = keyIds.slice(i, i + batchSize);
|
||||||
|
if (batch.length === 0) continue;
|
||||||
|
|
||||||
|
const batchData = await this.fetchUsageBatch(batch);
|
||||||
|
for (const [id, stats] of Object.entries(batchData)) {
|
||||||
|
usageMap.set(id, stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usageMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentCosts(): Promise<ApiKeyWithUsage[]> {
|
||||||
|
const pageSize = 50;
|
||||||
|
let page = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
const allItems: ApiKeyListItem[] = [];
|
||||||
|
|
||||||
|
while (page <= totalPages) {
|
||||||
|
const data = await this.fetchApiKeysPage(page, pageSize);
|
||||||
|
allItems.push(...data.items);
|
||||||
|
totalPages = data.pagination.totalPages || page;
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shareableItems = allItems.filter(user => !user.tags?.includes("noshare"));
|
||||||
|
const usageStats = await this.getUsageStats(shareableItems.map(item => item.id));
|
||||||
|
|
||||||
|
return shareableItems.map((item) => {
|
||||||
|
const stats = usageStats.get(item.id);
|
||||||
|
const cost = Number(stats?.cost ?? 0);
|
||||||
|
const formattedCost = stats?.formattedCost ?? `$${cost.toFixed(2)}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
usage: {
|
||||||
|
total: {
|
||||||
|
cost,
|
||||||
|
tokens: Number(stats?.tokens ?? 0),
|
||||||
|
inputTokens: Number(stats?.inputTokens ?? 0),
|
||||||
|
outputTokens: Number(stats?.outputTokens ?? 0),
|
||||||
|
cacheCreateTokens: Number(stats?.cacheCreateTokens ?? 0),
|
||||||
|
cacheReadTokens: Number(stats?.cacheReadTokens ?? 0),
|
||||||
|
requests: Number(stats?.requests ?? 0),
|
||||||
|
formattedCost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getKeyIdFromLegacy(apiKey: string): Promise<string> {
|
||||||
const response = await fetch(`${this.baseUrl}/apiStats/api/get-key-id`, {
|
const response = await fetch(`${this.baseUrl}/apiStats/api/get-key-id`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -150,6 +271,33 @@ export class ApiClient {
|
|||||||
|
|
||||||
return data.data.id;
|
return data.data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getKeyIdFromList(apiKey: string): Promise<string> {
|
||||||
|
const pageSize = 50;
|
||||||
|
let page = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
|
||||||
|
while (page <= totalPages) {
|
||||||
|
const data = await this.fetchApiKeysPage(page, pageSize);
|
||||||
|
const match = data.items.find(item => item.apiKey === apiKey);
|
||||||
|
if (match?.id) {
|
||||||
|
return match.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalPages = data.pagination.totalPages || page;
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Invalid API key or response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getKeyId(apiKey: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
return await this.getKeyIdFromLegacy(apiKey);
|
||||||
|
} catch (error) {
|
||||||
|
return await this.getKeyIdFromList(apiKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = new ApiClient();
|
export const apiClient = new ApiClient();
|
||||||
|
|||||||
Reference in New Issue
Block a user