feat(cloud): attribute planner token usage

This commit is contained in:
2026-07-13 23:10:21 +08:00
parent b4803f90e6
commit 40efa53411
16 changed files with 259 additions and 17 deletions
+7
View File
@@ -10,6 +10,7 @@ import type {
TaskListResponse,
TaskSubmissionPayload,
TaskStatus,
TokenUsageEvent,
UserListResponse,
UserSubmissionPolicy,
} from "./types";
@@ -204,6 +205,12 @@ export function getHostTokenUsage(hostId: string): Promise<HostTokenUsageSummary
);
}
export function listHostTokenUsageEvents(hostId: string): Promise<TokenUsageEvent[]> {
return request<TokenUsageEvent[]>(
`/v1/hosts/${encodeURIComponent(hostId)}/token-usage-events?limit=20&offset=0`,
);
}
export function listTasks(options?: {
status?: TaskStatus;
limit?: number;
+14
View File
@@ -134,3 +134,17 @@ export interface HostTokenUsageSummary {
reserved_tokens: number;
remaining_tokens: number | null;
}
export interface TokenUsageEvent {
id: string;
host_id: string;
usage_day: string;
task_id: string | null;
attempt: number | null;
provider: string;
model: string;
input_tokens: number | null;
output_tokens: number | null;
total_tokens: number;
occurred_at: string;
}
+30 -3
View File
@@ -5,6 +5,7 @@ import {
createUser,
getHostGovernancePolicy,
getHostTokenUsage,
listHostTokenUsageEvents,
getUserSubmissionPolicy,
listDevices,
listHosts,
@@ -20,6 +21,7 @@ import type {
DeviceRecord,
HostRecord,
HostTokenUsageSummary,
TokenUsageEvent,
UserRole,
} from "../types";
@@ -54,6 +56,7 @@ const hostSelfSubmissionEnabled = ref(true);
const hostMaxActiveTasks = ref("");
const hostDailyTokenBudget = ref("");
const hostUsage = ref<HostTokenUsageSummary | null>(null);
const hostUsageEvents = ref<TokenUsageEvent[]>([]);
const selectedUser = computed(
() => users.value.find((user) => user.id === selectedUserId.value) ?? null,
@@ -103,11 +106,13 @@ async function loadHostPolicy() {
if (!props.canAdminGovernance || !selectedHostId.value) return;
errorMessage.value = "";
try {
const [policy, usage] = await Promise.all([
const [policy, usage, events] = await Promise.all([
getHostGovernancePolicy(selectedHostId.value),
getHostTokenUsage(selectedHostId.value),
listHostTokenUsageEvents(selectedHostId.value),
]);
hostUsage.value = usage;
hostUsageEvents.value = events;
hostPolicyRevision.value = policy.revision;
hostSelfSubmissionEnabled.value = policy.self_submission_enabled;
hostMaxActiveTasks.value = policy.max_active_tasks?.toString() ?? "";
@@ -118,7 +123,12 @@ async function loadHostPolicy() {
hostSelfSubmissionEnabled.value = true;
hostMaxActiveTasks.value = "";
hostDailyTokenBudget.value = "";
hostUsage.value = await getHostTokenUsage(selectedHostId.value);
const [usage, events] = await Promise.all([
getHostTokenUsage(selectedHostId.value),
listHostTokenUsageEvents(selectedHostId.value),
]);
hostUsage.value = usage;
hostUsageEvents.value = events;
return;
}
showError(error, "failed to load Host policy");
@@ -264,7 +274,12 @@ async function saveHostPolicy() {
expected_revision: hostPolicyRevision.value ?? 0,
});
hostPolicyRevision.value = policy.revision;
hostUsage.value = await getHostTokenUsage(selectedHostId.value);
const [usage, events] = await Promise.all([
getHostTokenUsage(selectedHostId.value),
listHostTokenUsageEvents(selectedHostId.value),
]);
hostUsage.value = usage;
hostUsageEvents.value = events;
successMessage.value = `Host policy saved (revision ${policy.revision})`;
} catch (error) {
showError(error, "failed to save Host policy");
@@ -375,6 +390,18 @@ onMounted(() => void refresh());
{{ hostUsage.usage_day }}: used {{ hostUsage.used_tokens }}, reserved {{ hostUsage.reserved_tokens }},
remaining {{ hostUsage.remaining_tokens ?? "unmetered" }} tokens.
</p>
<table v-if="hostUsageEvents.length">
<thead><tr><th>Time</th><th>Provider / model</th><th>Tokens</th><th>Task</th></tr></thead>
<tbody>
<tr v-for="event in hostUsageEvents" :key="event.id">
<td class="dim">{{ new Date(event.occurred_at).toLocaleString() }}</td>
<td>{{ event.provider }} <span class="dim">{{ event.model }}</span></td>
<td>{{ event.total_tokens }}</td>
<td class="dim">{{ event.task_id ?? "local" }}<span v-if="event.attempt"> / #{{ event.attempt }}</span></td>
</tr>
</tbody>
</table>
<p v-else class="muted">No Cloud-proxy usage events recorded for this Host.</p>
</div>
</div>
</template>