diff --git a/web/e2e/business-flows.spec.ts b/web/e2e/business-flows.spec.ts
index 4aaec83a..c654ddfe 100644
--- a/web/e2e/business-flows.spec.ts
+++ b/web/e2e/business-flows.spec.ts
@@ -10,8 +10,10 @@ const upstreamURL = 'http://127.0.0.1:3108'
const discoveredModel = 'e2e-model-one'
const secondDiscoveredModel = 'e2e-model-two'
const rpmLimit = '37'
+const expectedUsageRequests = 3
test.use({ locale: 'en-US' })
+test.setTimeout(120_000)
test('critical management journey works through the embedded binary', async ({ page }) => {
const browserUpstreamRequests: string[] = []
@@ -23,6 +25,7 @@ test('critical management journey works through the embedded binary', async ({ p
})
let groupID = ''
+ let issuedAccessKey = ''
await test.step('login', async () => {
await page.goto('/login')
@@ -85,6 +88,9 @@ test('critical management journey works through the embedded binary', async ({ p
await createDrawer.getByLabel('Requests per minute').fill(rpmLimit)
await createDrawer.getByRole('button', { name: 'Save AccessKey' }).click()
await expect(createDrawer.getByText('Plaintext AccessKey', { exact: true })).toBeVisible()
+ await createDrawer.locator('[data-test="access-key-result-reveal"]').click({ timeout: 10_000 })
+ issuedAccessKey = await createDrawer.locator('.access-key-drawer__secret code').innerText()
+ expect(issuedAccessKey).toMatch(/^sk-gl-[0-9a-f]{32}$/)
await createDrawer.getByRole('button', { name: 'Close AccessKey editor' }).click()
const accessKeyRow = page.getByRole('row').filter({
@@ -116,6 +122,196 @@ test('critical management journey works through the embedded binary', async ({ p
await editDrawer.getByRole('button', { name: 'Close AccessKey editor' }).click()
})
+ await test.step('manage a global price override through the canonical deep link', async () => {
+ await page.goto('/settings/model-prices')
+ await expect(page).toHaveURL('/settings/model-prices')
+ await expect(page.getByRole('heading', { name: 'Model prices' })).toBeVisible()
+
+ const addPrice = page.locator('[data-test="model-price-add"]')
+ await addPrice.click()
+ const addDrawer = page.getByRole('dialog', { name: 'Add model price override' })
+ await addDrawer.getByLabel('Model pattern').fill('*')
+ await addDrawer.getByLabel('Uncached input').fill('1')
+ await expect(addDrawer.getByRole('button', { name: 'Save override' })).toBeDisabled()
+ await addDrawer.getByLabel('I understand that * shadows every built-in price rule.').check()
+ await addDrawer.getByRole('button', { name: 'Save override' }).click()
+
+ const overrideRow = page.locator('[data-test="override-price-row-0"]')
+ await expect(overrideRow).toContainText('*')
+ await expect(overrideRow).toContainText('$1')
+
+ const editPrice = page.locator('[data-test="override-price-edit-0"]')
+ await editPrice.click()
+ const editDrawer = page.getByRole('dialog', { name: 'Edit model price override' })
+ await expect(editDrawer.getByLabel('Model pattern')).toHaveAttribute('readonly', '')
+ await editDrawer.getByLabel('Output').fill('2')
+ await editDrawer.getByLabel('I understand that * shadows every built-in price rule.').check()
+ await editDrawer.getByRole('button', { name: 'Save override' }).click()
+ await expect(editPrice).toBeFocused()
+ await expect(page.locator('[data-test="override-price-row-0"]')).toContainText('$2')
+
+ const resetPrice = page.locator('[data-test="model-price-reset-open"]')
+ await resetPrice.click()
+ const resetDialog = page.getByRole('dialog', {
+ name: 'Reset this model price override?',
+ })
+ await resetDialog.getByRole('button', { name: 'Cancel' }).click()
+ await expect(resetPrice).toBeFocused()
+ await resetPrice.click()
+ await resetDialog.getByRole('button', { name: 'Reset override' }).click()
+ await expect(page.getByText('No overrides configured', { exact: true })).toBeVisible()
+ })
+
+ await test.step('produce persisted complete, partial, and missing usage safely', async () => {
+ for (const [content, stream] of [
+ ['missing-usage', false],
+ ['partial-usage', true],
+ ['complete-usage', false],
+ ] as const) {
+ const response = await page.request.post('/v1/chat/completions', {
+ headers: {
+ Authorization: `Bearer ${issuedAccessKey}`,
+ 'Content-Type': 'application/json',
+ },
+ data: {
+ model: discoveredModel,
+ messages: [{ role: 'user', content }],
+ stream,
+ },
+ })
+ expect(response.status()).toBe(200)
+ }
+
+ await expect
+ .poll(
+ async () => {
+ const response = await page.request.get('/api/logs', {
+ headers: { Authorization: `Bearer ${authKey}` },
+ })
+ if (!response.ok()) return -1
+ const payload = (await response.json()) as { data?: { items?: unknown[] } }
+ return payload.data?.items?.length ?? -1
+ },
+ { message: 'wait for all request logs to persist' },
+ )
+ .toBe(expectedUsageRequests)
+
+ await expect
+ .poll(
+ async () => {
+ const response = await page.request.get('/api/usage?range=24h', {
+ headers: { Authorization: `Bearer ${authKey}` },
+ })
+ if (!response.ok()) return { requestCount: -1, status: response.status() }
+ const payload = (await response.json()) as {
+ data?: { summary?: { request_count?: number } }
+ }
+ return {
+ requestCount: payload.data?.summary?.request_count ?? -1,
+ status: response.status(),
+ }
+ },
+ { message: 'wait for the usage aggregate to include all persisted requests' },
+ )
+ .toEqual({ requestCount: expectedUsageRequests, status: 200 })
+ })
+
+ await test.step('review usage quality, request detail usage, and the Home summary', async () => {
+ await page.goto('/monitor?tab=usage&range=24h')
+ await expect(page).toHaveURL('/monitor?tab=usage&range=24h')
+ await expect(page.locator('[data-test="usage-quality-missing"]')).toContainText('1')
+ await expect(page.locator('[data-test="usage-quality-partial"]')).toContainText('1')
+ await expect(page.locator('[data-test="usage-quality-unpriced"]')).toContainText('3')
+ await expect(page.locator('[data-test="usage-kpi-cost"]')).toContainText('Unknown')
+ await expect(page.locator('[data-test="usage-kpi-cost"]')).not.toContainText('$0')
+ await expect(page.locator('[data-test="usage-kpi-cost"]')).not.toContainText('Free')
+ await expect(page.locator('[data-test="usage-scope"]')).toContainText('current process')
+ await expect(page.locator('[data-test="usage-prices-link"]')).toHaveAttribute(
+ 'href',
+ '/settings/model-prices',
+ )
+
+ await page.goto('/monitor?tab=logs')
+ const firstLogRow = page.locator('[data-test^="log-row-"]').first()
+ await expect(firstLogRow).toBeVisible()
+ await firstLogRow.getByRole('button', { name: 'View details' }).click()
+ const logDrawer = page.getByRole('dialog', { name: 'Request log details' })
+ const logUsage = logDrawer.locator('[data-test="log-usage-cost"]')
+ await expect(logUsage).toContainText('Complete usage')
+ await expect(logUsage).toContainText('Estimated cost unknown')
+ await expect(logUsage).toContainText('Uncached input')
+ await expect(logUsage).toContainText('11')
+ await expect(logDrawer.locator('[data-test="log-usage-prices-link"]')).toHaveAttribute(
+ 'href',
+ '/settings/model-prices',
+ )
+ await logDrawer.getByRole('button', { name: 'Close request log details' }).click()
+
+ await page.goto('/')
+ await expect(page.locator('[data-test="home-usage-requests"]')).toContainText(
+ String(expectedUsageRequests),
+ )
+ await expect(page.locator('[data-test="home-usage-cost"]')).toContainText('Unknown')
+ await expect(page.locator('[data-test="home-usage-detail"]')).toHaveAttribute(
+ 'href',
+ '/monitor?tab=usage&range=24h',
+ )
+
+ const visibleText = await page.locator('body').innerText()
+ for (const secret of [authKey, firstUpstreamKey, secondUpstreamKey, issuedAccessKey]) {
+ expect(visibleText).not.toContain(secret)
+ }
+ })
+
+ await test.step('verify responsive light and dark layouts with table-local overflow', async () => {
+ const viewports = [
+ { width: 375, height: 812 },
+ { width: 768, height: 900 },
+ { width: 1024, height: 900 },
+ { width: 1440, height: 900 },
+ ] as const
+
+ for (const theme of ['light', 'dark'] as const) {
+ await page.evaluate((value) => localStorage.setItem('gpt-load.theme', value), theme)
+ for (const viewport of viewports) {
+ await page.setViewportSize(viewport)
+ await page.goto('/')
+ await expect(page.locator('html')).toHaveAttribute('data-theme', theme)
+ await expect(page.locator('[data-test="home-usage-requests"]')).toBeVisible()
+
+ const layout = await page.evaluate(() => {
+ const overview = document.querySelector('.home-overview')?.getBoundingClientRect()
+ const usage = document.querySelector('.usage-summary-section')?.getBoundingClientRect()
+ const groups = document.querySelector('.groups-section')?.getBoundingClientRect()
+ return {
+ documentWidth: document.documentElement.scrollWidth,
+ overviewBottom: overview?.bottom ?? -1,
+ usageBottom: usage?.bottom ?? -1,
+ usageTop: usage?.top ?? -1,
+ groupsTop: groups?.top ?? -1,
+ viewportWidth: window.innerWidth,
+ }
+ })
+ expect(layout.documentWidth).toBeLessThanOrEqual(layout.viewportWidth)
+ expect(layout.usageTop).toBeGreaterThanOrEqual(layout.overviewBottom)
+ expect(layout.groupsTop).toBeGreaterThanOrEqual(layout.usageBottom)
+ }
+ }
+
+ await page.setViewportSize({ width: 375, height: 812 })
+ await page.goto('/settings/model-prices')
+ const priceTable = page.locator('.data-table__container').first()
+ await expect(priceTable).toBeVisible()
+ const overflow = await priceTable.evaluate((table) => ({
+ client: table.clientWidth,
+ scroll: table.scrollWidth,
+ document: document.documentElement.scrollWidth,
+ viewport: window.innerWidth,
+ }))
+ expect(overflow.scroll).toBeGreaterThan(overflow.client)
+ expect(overflow.document).toBeLessThanOrEqual(overflow.viewport)
+ })
+
await test.step('inspect current route', async () => {
await page.setViewportSize({ width: 375, height: 812 })
await page.goto('/monitor?tab=inspector')
diff --git a/web/e2e/start-e2e-server.mjs b/web/e2e/start-e2e-server.mjs
index c046053d..09b12150 100644
--- a/web/e2e/start-e2e-server.mjs
+++ b/web/e2e/start-e2e-server.mjs
@@ -36,13 +36,67 @@ const modelList = JSON.stringify({
},
],
})
-const fakeUpstream = createServer((request, response) => {
+async function requestBody(request) {
+ const chunks = []
+ for await (const chunk of request) chunks.push(chunk)
+ return Buffer.concat(chunks).toString('utf8')
+}
+
+const fakeUpstream = createServer(async (request, response) => {
if (request.method === 'GET' && request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' })
response.end(modelList)
return
}
+ if (request.method === 'POST' && request.url === '/v1/chat/completions') {
+ let payload
+ try {
+ payload = JSON.parse(await requestBody(request))
+ } catch {
+ response.writeHead(400, { 'content-type': 'application/json' })
+ response.end('{"error":{"message":"invalid request"}}')
+ return
+ }
+ const content = payload.messages?.[0]?.content
+ if (content === 'partial-usage' && payload.stream === true) {
+ response.writeHead(200, {
+ 'cache-control': 'no-cache',
+ connection: 'keep-alive',
+ 'content-type': 'text/event-stream',
+ })
+ response.write(
+ 'data: {"id":"chatcmpl-e2e-partial","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"partial"}}],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}\n\n',
+ )
+ response.end('data: [DONE]\n\n')
+ return
+ }
+
+ const result = {
+ id: `chatcmpl-e2e-${content === 'missing-usage' ? 'missing' : 'complete'}`,
+ object: 'chat.completion',
+ created: 1_735_689_600,
+ model: 'e2e-model-one',
+ choices: [
+ {
+ index: 0,
+ message: { role: 'assistant', content: 'ok' },
+ finish_reason: 'stop',
+ },
+ ],
+ }
+ if (content !== 'missing-usage') {
+ result.usage = {
+ prompt_tokens: 11,
+ completion_tokens: 7,
+ total_tokens: 18,
+ }
+ }
+ response.writeHead(200, { 'content-type': 'application/json' })
+ response.end(JSON.stringify(result))
+ return
+ }
+
response.writeHead(404, { 'content-type': 'application/json' })
response.end('{"error":"not_found"}')
})
diff --git a/web/src/api/control/usage.test.ts b/web/src/api/control/usage.test.ts
index 64ccf892..0ad971b6 100644
--- a/web/src/api/control/usage.test.ts
+++ b/web/src/api/control/usage.test.ts
@@ -86,6 +86,26 @@ describe('Usage control API', () => {
})
})
+ it('accepts the current partial bucket when its start is inside the report range', () => {
+ const currentBucketReport = {
+ ...report,
+ observed_at: '2026-07-27T12:21:48Z',
+ range: {
+ ...report.range,
+ to: '2026-07-27T12:21:48Z',
+ },
+ series: [
+ {
+ ...report.series[0],
+ bucket_start: '2026-07-27T12:00:00Z',
+ bucket_end: '2026-07-27T13:00:00Z',
+ },
+ ],
+ }
+
+ expect(projectUsageReport(currentBucketReport)).toEqual(currentBucketReport)
+ })
+
it.each([
['non-object report', null],
['missing aggregate field', { ...report, summary: { ...aggregate, total_tokens: undefined } }],
diff --git a/web/src/api/control/usage.ts b/web/src/api/control/usage.ts
index 1836dbe7..b3b63539 100644
--- a/web/src/api/control/usage.ts
+++ b/web/src/api/control/usage.ts
@@ -180,7 +180,7 @@ export function projectUsageReport(value: unknown): UsageReportDto {
const bucketEnd = timestamp(item.bucket_end)
const start = timestampMs(bucketStart)
const end = timestampMs(bucketEnd)
- if (start < previousBucketEnd || start >= end || end > timestampMs(rangeTo)) {
+ if (start < previousBucketEnd || start >= end || start >= timestampMs(rangeTo)) {
throw new InvalidResponseError()
}
previousBucketEnd = end
diff --git a/web/src/features/home/HomeView.test.ts b/web/src/features/home/HomeView.test.ts
index 384da251..98bb4173 100644
--- a/web/src/features/home/HomeView.test.ts
+++ b/web/src/features/home/HomeView.test.ts
@@ -2,6 +2,7 @@ import { QueryClient } from '@tanstack/vue-query'
import { flushPromises } from '@vue/test-utils'
import { ApiError } from '@/api/errors'
+import type { UsageReportDto } from '@/api/control/usage'
import { controlQueryKeys } from '@/app/query-keys'
import type { AccessKeyDto, GroupSummary, RuntimeHealthDto } from '@/api/control/types'
import { FakeApi } from '@/test/fake-api'
@@ -59,7 +60,37 @@ const healthFixture: RuntimeHealthDto = {
},
}
+const usageFixture: UsageReportDto = {
+ observed_at: '2026-07-25T10:00:01Z',
+ range: {
+ from: '2026-07-24T10:00:00Z',
+ to: '2026-07-25T10:00:00Z',
+ granularity: 'hour',
+ },
+ filters: { group_id: null, model: '' },
+ summary: {
+ request_count: 4,
+ success_count: 3,
+ failure_count: 1,
+ uncached_input_tokens: 10,
+ cache_read_tokens: 2,
+ cache_write_5m_tokens: 0,
+ cache_write_1h_tokens: 0,
+ output_tokens: 5,
+ total_tokens: 17,
+ estimated_cost_usd: 0.0025,
+ usage_missing_count: 0,
+ partial_count: 0,
+ unpriced_request_count: 0,
+ },
+ series: [],
+ breakdown: [],
+ breakdown_truncated: false,
+ request_log: healthFixture.request_log,
+}
+
async function mountHome(api: FakeApi, origin?: string, queryClient?: QueryClient) {
+ api.when('/api/usage?range=24h').resolve(usageFixture)
const mounted = await mountApp(HomeView, {
api,
queryClient: queryClient ?? new QueryClient({ defaultOptions: { queries: { retry: false } } }),
@@ -97,6 +128,26 @@ describe('HomeView', () => {
expect(wrapper.text()).toContain('无法加载 Group')
})
+ it('keeps overview and Groups mounted when the Usage summary fails independently', async () => {
+ const api = new FakeApi()
+ api.when('/api/groups').resolve([groupFixture])
+ api.when('/api/health').resolve(healthFixture)
+ api.when('/api/access-keys').resolve([accessKeyFixture])
+ api
+ .when('/api/usage?range=24h')
+ .reject(new ApiError(500, 'INTERNAL_SERVER_ERROR', 'usage-secret-canary'))
+
+ const wrapper = await mountHome(api)
+
+ expect(wrapper.text()).toContain('在线')
+ expect(wrapper.text()).toContain('Base URL')
+ expect(wrapper.get('[data-group-id="1"]').text()).toContain('Example')
+ expect(wrapper.get('[data-test="home-usage-error"]').text()).toContain(
+ '无法加载最近 24 小时用量摘要',
+ )
+ expect(wrapper.text()).not.toContain('usage-secret-canary')
+ })
+
it('retains stale Health data when a background refresh fails', async () => {
const api = new FakeApi()
api.when('/api/groups').resolve([groupFixture])
@@ -153,7 +204,7 @@ describe('HomeView', () => {
expect(wrapper.get('[aria-label="复制 Base URL"]').attributes()).not.toHaveProperty('disabled')
})
- it('renders real health counts and no fabricated dashboard metrics', async () => {
+ it('renders real health and usage counts without fabricated health metrics or a chart', async () => {
const api = new FakeApi()
api.when('/api/groups').resolve([groupFixture])
api.when('/api/health').resolve(healthFixture)
@@ -164,7 +215,9 @@ describe('HomeView', () => {
expect(wrapper.text()).toContain('可用 1')
expect(wrapper.text()).toContain('冷却 1')
expect(wrapper.text()).toContain('修订 8')
- expect(wrapper.text()).not.toMatch(/成功率|健康率|吞吐|Token|费用|趋势/)
+ expect(wrapper.text()).toContain('最近 24 小时')
+ expect(wrapper.text()).not.toMatch(/健康率|吞吐|趋势/)
+ expect(wrapper.find('[data-test="usage-sparkline"]').exists()).toBe(false)
expect(wrapper.html()).not.toContain('ACCESS_KEY_CANARY')
})
diff --git a/web/src/features/home/HomeView.vue b/web/src/features/home/HomeView.vue
index d2b7f223..d644e9c1 100644
--- a/web/src/features/home/HomeView.vue
+++ b/web/src/features/home/HomeView.vue
@@ -18,6 +18,7 @@ import SurfaceCard from '@/components/ui/SurfaceCard.vue'
import ConnectionCard from './ConnectionCard.vue'
import GroupCard from './GroupCard.vue'
+import UsageSummaryCard from './UsageSummaryCard.vue'
const props = withDefaults(defineProps<{ origin?: string }>(), {
origin: () => window.location.origin,
@@ -157,6 +158,8 @@ const observedAt = computed(() => {
+
+