Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f649419
feat(stats): define usage event and message contracts
k1yt Jul 18, 2026
c671606
feat(stats): add append-only local usage store and aggregation
k1yt Jul 18, 2026
662121f
feat(stats): record final usage for each API attempt
k1yt Jul 18, 2026
ddc170e
feat(stats): expose stats query export and clear handlers
k1yt Jul 18, 2026
461852a
feat(stats): add slash entry and statistics webview
k1yt Jul 18, 2026
2a54783
fix(stats): resolve blockers B1/B2/B3 and highs H1/H3
k1yt Jul 19, 2026
ca833ef
feat(stats): add autocomplete entry and time-axis groupBy in UI
k1yt Jul 19, 2026
eb8a3d9
test(stats): add coverage tests for UsageStatsService, UsageHeatmap, …
k1yt Jul 19, 2026
908af59
i18n(stats): add translations for 17 languages
k1yt Jul 19, 2026
cca4571
fix(i18n): remove BOM from package.nls.ca.json
k1yt Jul 19, 2026
df747af
fix(i18n): remove BOM from all package.nls locale files
k1yt Jul 19, 2026
c0b3c3e
fix(i18n): restore missing opening brace in all package.nls locale files
k1yt Jul 19, 2026
92c1b8b
i18n(stats): apply CodeRabbit translation review fixes (de, fr, vi, z…
k1yt Jul 19, 2026
2bb3151
refactor(stats): convert all Korean comments to English
k1yt Jul 19, 2026
c07d388
feat(dashboard): remove /stats command and add Dashboard sidebar entry
k1yt Jul 19, 2026
8d62368
feat(dashboard): add DashboardView with summary, time range, and brea…
k1yt Jul 19, 2026
0c836e8
feat(dashboard): add session list with titles and model/provider filters
k1yt Jul 19, 2026
53b5183
feat(dashboard): add session detail with expandable API call list
k1yt Jul 19, 2026
d8c68e8
feat(dashboard): add translations for all 17 languages
k1yt Jul 19, 2026
d07f760
test(stats): remove stale 'stats' command test assertions
k1yt Jul 19, 2026
5915d18
refactor(dashboard): remove orphaned StatsView, i18n relative time, e…
k1yt Jul 19, 2026
f320d7d
feat(dashboard): default Custom date range to yesterday-today
k1yt Jul 19, 2026
d47bb2b
fix(providers): add totalCost calculation using user-configured pricing
k1yt Jul 19, 2026
4a10fc6
feat(dashboard): compute missing costs at query time and fix session …
k1yt Jul 19, 2026
cd2ec15
feat(dashboard): add usage dashboard with mode column, multi-model ag…
k1yt Jul 20, 2026
8855f2d
feat(heatmap): blue gradient 6 levels, white borders, and 221 new tests
k1yt Jul 20, 2026
1bae4f4
feat(dashboard): responsive heatmap, 30d/60d/120d/360d ranges, CI fix…
k1yt Jul 20, 2026
eb51bff
feat(stats): make UsageHeatmap self-fetching for independent range se…
k1yt Jul 20, 2026
0bb0ece
test(stats): add comprehensive DashboardView test suite for codecov p…
k1yt Jul 20, 2026
9c4c374
fix(stats): remove unused variables in DashboardView.spec.tsx to fix …
k1yt Jul 20, 2026
8b11316
fix(stats): correct totalTokens calculation, provider pricing, and da…
k1yt Jul 20, 2026
327e88b
fix(stats): remove day axis from breakdown groupBy to eliminate dupli…
k1yt Jul 20, 2026
9189f27
feat(stats): add endpoint domain extraction for provider identificati…
k1yt Jul 20, 2026
aa4e017
fix(terminal): retry with execa when shell integration loses command
k1yt Jul 20, 2026
117274f
fix(stats): update MiMo pricing, remove session filters, add NDJSON c…
k1yt Jul 21, 2026
9bc875c
feat(dashboard): add multi-window refresh, cache ratio estimation, an…
k1yt Jul 24, 2026
0601728
fix(stats): pass all CI checks after rebase onto main
k1yt Jul 24, 2026
22b24d9
fix(dashboard): remove unknownEventCount display and utility scripts
k1yt Jul 24, 2026
815d315
fix(ci): increase e2e timeout and add provider totalCost tests
k1yt Jul 24, 2026
54664ab
fix(ci): pass test:coverage
k1yt Jul 24, 2026
83c6681
fix(ci): revert e2e timeout + add coverage tests
k1yt Jul 24, 2026
4d602b1
fix(ci): break terminal-reuse fixture re-match loop
k1yt Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion apps/vscode-e2e/src/fixtures/terminal-reuse-shell-race.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ import { toolResultContains } from "./tool-result"
export function addTerminalReuseShellRaceFixtures(mock: InstanceType<typeof LLMock>) {
// First command completes — model issues a second command on the same terminal.
// With the temp-script fix, both commands now deliver real output.
// Single-fire guard: `toolResultContains` is a permanent-history existence check, so once the
// first command's result is in history this predicate would match on EVERY subsequent re-prompt.
// If the second command's result does not satisfy Fixture B, Fixture A would keep re-emitting the
// second command forever (60s test timeout). Fire exactly once.
let secondCommandIssued = false
mock.addFixture({
match: {
predicate: (req) => toolResultContains(req, "call_terminal_reuse_001", ["first", "Exit code: 0"]),
predicate: (req) => {
if (secondCommandIssued) return false
if (!toolResultContains(req, "call_terminal_reuse_001", ["first", "Exit code: 0"])) return false
// Match confirmed — the response below will be emitted. Latch so we never match again.
secondCommandIssued = true
return true
},
},
response: {
toolCalls: [
Expand Down
323 changes: 323 additions & 0 deletions packages/types/src/__tests__/usage-stats.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import {
UsageEventStatus,
UsageValueSource,
InclusionRule,
SourcedNumber,
UsageEventV1,
StatsQuery,
StatsBucket,
StatsSnapshot,
} from "../usage-stats.js"

describe("usage-stats schemas", () => {
// ── Enums ────────────────────────────────────────────────────────────

describe("UsageEventStatus", () => {
it("should accept all valid statuses", () => {
expect(UsageEventStatus.parse("completed")).toBe("completed")
expect(UsageEventStatus.parse("failed")).toBe("failed")
expect(UsageEventStatus.parse("cancelled")).toBe("cancelled")
})

it("should reject invalid status", () => {
expect(() => UsageEventStatus.parse("success")).toThrow()
})
})

describe("UsageValueSource", () => {
it("should accept all valid sources", () => {
expect(UsageValueSource.parse("provider")).toBe("provider")
expect(UsageValueSource.parse("estimated")).toBe("estimated")
expect(UsageValueSource.parse("backfilled")).toBe("backfilled")
})

it("should reject invalid source", () => {
expect(() => UsageValueSource.parse("guessed")).toThrow()
})
})

describe("InclusionRule", () => {
it("should accept all valid rules", () => {
expect(InclusionRule.parse("included")).toBe("included")
expect(InclusionRule.parse("excluded")).toBe("excluded")
expect(InclusionRule.parse("unknown")).toBe("unknown")
})
})

// ── SourcedNumber ─────────────────────────────────────────────────────

describe("SourcedNumber", () => {
it("should parse a valid SourcedNumber", () => {
const result = SourcedNumber.parse({ value: 42, source: "provider" })
expect(result).toEqual({ value: 42, source: "provider" })
})

it("should reject missing source", () => {
expect(() => SourcedNumber.parse({ value: 42 })).toThrow()
})

it("should reject missing value", () => {
expect(() => SourcedNumber.parse({ source: "estimated" })).toThrow()
})
})

// ── UsageEventV1 ────────────────────────────────────────────────────────

describe("UsageEventV1", () => {
const validEvent = {
schemaVersion: 1,
eventId: "evt-001",
idempotencyKey: "idem-001",
occurredAt: "2026-07-18T12:00:00.000Z",
timezoneOffsetMinutes: -540,
status: "completed",
attempt: 1,
taskId: "task-001",
provider: "anthropic",
model: "claude-sonnet-4-20250514",
mode: "code",
usage: {
inputTokens: { value: 1000, source: "provider" },
outputTokens: { value: 500, source: "provider" },
costUsd: { value: 0.015, source: "provider" },
},
semantics: {
cacheReadInInput: "included",
cacheWriteInInput: "included",
reasoningInOutput: "excluded",
},
provenance: "live",
}

it("should parse a valid complete event", () => {
const result = UsageEventV1.parse(validEvent)
expect(result.eventId).toBe("evt-001")
expect(result.schemaVersion).toBe(1)
expect(result.usage.inputTokens?.value).toBe(1000)
})

it("should accept optional parentTaskId", () => {
const result = UsageEventV1.parse({ ...validEvent, parentTaskId: "task-000" })
expect(result.parentTaskId).toBe("task-000")
})

it("should work without optional usage fields", () => {
const minimal = { ...validEvent, usage: {} }
const result = UsageEventV1.parse(minimal)
expect(result.usage.inputTokens).toBeUndefined()
})

it("should accept backfilled provenance", () => {
const result = UsageEventV1.parse({ ...validEvent, provenance: "history-backfill" })
expect(result.provenance).toBe("history-backfill")
})

it("should reject schemaVersion !== 1", () => {
expect(() => UsageEventV1.parse({ ...validEvent, schemaVersion: 2 })).toThrow()
})

it("should reject missing semantics", () => {
const { semantics: _semantics, ...withoutSemantics } = validEvent
expect(() => UsageEventV1.parse(withoutSemantics)).toThrow()
})

it("should reject invalid provenance", () => {
expect(() => UsageEventV1.parse({ ...validEvent, provenance: "imported" })).toThrow()
})

it("should reject missing required fields (eventId)", () => {
const { eventId: _eventId, ...withoutEventId } = validEvent
expect(() => UsageEventV1.parse(withoutEventId)).toThrow()
})

it("should accept attempt of 0 (no min constraint in V1)", () => {
// z.number() accepts negatives, but attempt should be >= 0 logically
// This test confirms the schema accepts any number (no min constraint in V1)
const result = UsageEventV1.parse({ ...validEvent, attempt: 0 })
expect(result.attempt).toBe(0)
})
})

// ── StatsQuery ───────────────────────────────────────────────────────

describe("StatsQuery", () => {
it("should parse a valid query with preset", () => {
const result = StatsQuery.parse({
preset: "7d",
timezone: "Asia/Seoul",
groupBy: ["day"],
})
expect(result.preset).toBe("7d")
expect(result.includeCancelled).toBe(false) // default
})

it("should parse a query with from/to range", () => {
const result = StatsQuery.parse({
from: "2026-07-01T00:00:00Z",
to: "2026-07-18T00:00:00Z",
timezone: "UTC",
groupBy: ["provider", "model"],
})
expect(result.from).toBe("2026-07-01T00:00:00Z")
expect(result.groupBy).toHaveLength(2)
})

it("should default includeCancelled to false", () => {
const result = StatsQuery.parse({
timezone: "UTC",
groupBy: [],
})
expect(result.includeCancelled).toBe(false)
})

it("should accept includeCancelled: true", () => {
const result = StatsQuery.parse({
timezone: "UTC",
groupBy: [],
includeCancelled: true,
})
expect(result.includeCancelled).toBe(true)
})

it("should reject more than 3 groupBy dimensions", () => {
expect(() =>
StatsQuery.parse({
timezone: "UTC",
groupBy: ["day", "week", "month", "provider"],
}),
).toThrow()
})

it("should reject invalid preset", () => {
expect(() =>
StatsQuery.parse({
preset: "90d",
timezone: "UTC",
groupBy: [],
}),
).toThrow()
})

it("should reject missing timezone", () => {
expect(() =>
StatsQuery.parse({
groupBy: [],
}),
).toThrow()
})

it("should reject invalid groupBy dimension", () => {
expect(() =>
StatsQuery.parse({
timezone: "UTC",
groupBy: ["hour"],
}),
).toThrow()
})
})

// ── StatsBucket ──────────────────────────────────────────────────────

describe("StatsBucket", () => {
const validBucket = {
key: { day: "2026-07-18" },
events: 10,
completedCalls: 8,
failedCalls: 1,
cancelledCalls: 1,
inputTokens: 5000,
outputTokens: 2500,
cacheReadTokens: 1000,
cacheWriteTokens: 500,
reasoningTokens: 200,
totalTokens: 7500,
costUsd: 0.075,
unknownEventCount: 0,
}

it("should parse a valid bucket", () => {
const result = StatsBucket.parse(validBucket)
expect(result.events).toBe(10)
expect(result.key.day).toBe("2026-07-18")
})

it("should reject missing required numeric field", () => {
const { costUsd: _costUsd, ...withoutCost } = validBucket
expect(() => StatsBucket.parse(withoutCost)).toThrow()
})

it("should accept empty key record", () => {
const result = StatsBucket.parse({ ...validBucket, key: {} })
expect(Object.keys(result.key)).toHaveLength(0)
})
})

// ── StatsSnapshot ─────────────────────────────────────────────────────

describe("StatsSnapshot", () => {
const validQuery = {
timezone: "UTC",
groupBy: ["day"],
}
const validBucket = {
key: { day: "2026-07-18" },
events: 5,
completedCalls: 4,
failedCalls: 1,
cancelledCalls: 0,
inputTokens: 2000,
outputTokens: 1000,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
totalTokens: 3000,
costUsd: 0.03,
unknownEventCount: 0,
}
const validSnapshot = {
query: validQuery,
generatedAt: "2026-07-18T12:00:00.000Z",
buckets: [validBucket],
totals: validBucket,
coverage: {
firstEventAt: "2026-07-01T00:00:00.000Z",
lastEventAt: "2026-07-18T12:00:00.000Z",
recordingPaused: false,
backfilledEventCount: 0,
},
}

it("should parse a valid snapshot", () => {
const result = StatsSnapshot.parse(validSnapshot)
expect(result.buckets).toHaveLength(1)
expect(result.coverage.recordingPaused).toBe(false)
})

it("should accept empty buckets array", () => {
const result = StatsSnapshot.parse({ ...validSnapshot, buckets: [] })
expect(result.buckets).toHaveLength(0)
})

it("should accept optional firstEventAt/lastEventAt omitted", () => {
const result = StatsSnapshot.parse({
...validSnapshot,
coverage: {
recordingPaused: true,
backfilledEventCount: 0,
},
})
expect(result.coverage.firstEventAt).toBeUndefined()
expect(result.coverage.lastEventAt).toBeUndefined()
})

it("should reject missing coverage", () => {
const { coverage: _coverage, ...withoutCoverage } = validSnapshot
expect(() => StatsSnapshot.parse(withoutCoverage)).toThrow()
})

it("should reject missing totals", () => {
const { totals: _totals, ...withoutTotals } = validSnapshot
expect(() => StatsSnapshot.parse(withoutTotals)).toThrow()
})
})
})
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from "./provider-settings.js"
export * from "./task.js"
export * from "./todo.js"
export * from "./skills.js"
export * from "./usage-stats.js"
export * from "./rules.js"
export * from "./marketplace.js"
export * from "./telemetry.js"
Expand Down
26 changes: 6 additions & 20 deletions packages/types/src/providers/mimo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,10 @@ export const mimoModels = {
supportsImages: false, // Pro series is text-only
supportsPromptCache: false,
preserveReasoning: true,
inputPrice: 1.0, // $1.00/1M tokens (cache miss, ≤256K)
outputPrice: 3.0, // $3.00/1M tokens (≤256K)
cacheReadsPrice: 0.2, // $0.20/1M tokens (cache hit, ≤256K)
inputPrice: 0.435, // $0.435/1M tokens
outputPrice: 0.87, // $0.87/1M tokens
cacheReadsPrice: 0.0036, // $0.0036/1M tokens
cacheWritesPrice: 0, // Free for limited time
// MiMo charges 2x above 256K context
longContextPricing: {
thresholdTokens: 256_000,
inputPriceMultiplier: 2,
outputPriceMultiplier: 2,
cacheReadsPriceMultiplier: 2,
},
description:
"MiMo V2.5 Pro - Xiaomi's flagship reasoning model with 1M context, deep thinking, tool calling, and structured output.",
},
Expand All @@ -41,17 +34,10 @@ export const mimoModels = {
supportsImages: true, // Full-modal: text, image, audio, video input
supportsPromptCache: false,
preserveReasoning: true,
inputPrice: 0.4, // $0.40/1M tokens (cache miss, ≤256K)
outputPrice: 2.0, // $2.00/1M tokens (≤256K)
cacheReadsPrice: 0.08, // $0.08/1M tokens (cache hit, ≤256K)
inputPrice: 0.14, // $0.14/1M tokens
outputPrice: 0.28, // $0.28/1M tokens
cacheReadsPrice: 0.0028, // $0.0028/1M tokens
cacheWritesPrice: 0, // Free for limited time
// MiMo charges 2x above 256K context
longContextPricing: {
thresholdTokens: 256_000,
inputPriceMultiplier: 2,
outputPriceMultiplier: 2,
cacheReadsPriceMultiplier: 2,
},
description:
"MiMo V2.5 - Full-modal understanding model (text, image, audio, video) with 1M context, deep thinking, tool calling, and structured output.",
},
Expand Down
Loading
Loading