copilot-api / tests /token-usage.test.ts
imspsycho's picture
Initial upload from Google Colab
98c9143 verified
Raw
History Blame Contribute Delete
10.4 kB
import {
afterEach,
beforeEach,
describe,
expect,
setSystemTime,
test,
} from "bun:test"
import { Hono } from "hono"
import { requestContext } from "~/lib/request-context"
import { state } from "~/lib/state"
import {
closeUsageStore,
createCopilotTokenUsageRecorder,
normalizeOpenAIUsage,
recordTokenUsageEvent,
type TokenUsageDailySummary,
type TokenUsageEventsPage,
type TokenUsageSummary,
} from "~/lib/token-usage"
import { traceIdMiddleware } from "~/lib/trace"
import { tokenUsageRoute } from "~/routes/token-usage/route"
const DB_PATH_ENV = "COPILOT_API_SQLITE_DB_PATH"
beforeEach(async () => {
process.env[DB_PATH_ENV] = ":memory:"
state.userName = "copilot-login"
await closeUsageStore()
})
afterEach(async () => {
await closeUsageStore()
setSystemTime()
state.userName = undefined
Reflect.deleteProperty(process.env, DB_PATH_ENV)
})
function createTokenUsageApp(): Hono {
const app = new Hono()
app.use(traceIdMiddleware)
app.route("/token-usage", tokenUsageRoute)
return app
}
async function fetchEventsPage(pageSize = 20): Promise<TokenUsageEventsPage> {
const response = await createTokenUsageApp().request(
`/token-usage/events?period=day&page=1&page_size=${pageSize}`,
)
expect(response.status).toBe(200)
return (await response.json()) as TokenUsageEventsPage
}
function localDate(year: number, month: number, day: number, hour = 12): Date {
return new Date(year, month, day, hour, 0, 0, 0)
}
function localDateLabel(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, "0")
const day = String(date.getDate()).padStart(2, "0")
return `${year}-${month}-${day}`
}
describe("token usage storage", () => {
test("normalizes OpenAI cache creation usage details", () => {
expect(
normalizeOpenAIUsage({
completion_tokens: 10,
prompt_tokens: 100,
prompt_tokens_details: {
cache_creation_input_tokens: 20,
cached_tokens: 12,
},
total_tokens: 110,
}),
).toEqual({
cache_creation_input_tokens: 20,
cache_read_input_tokens: 12,
input_tokens: 68,
output_tokens: 10,
total_tokens: 110,
})
})
test("records trace id and prefers x-session-affinity for session id", async () => {
requestContext.run(
{
parentSessionId: undefined,
sessionAffinity: "opencode-session",
startTime: Date.now(),
traceId: "trace-123",
userAgent: "test",
},
() => {
recordTokenUsageEvent({
endpoint: "messages",
input_tokens: 10,
model: "gpt-test",
output_tokens: 5,
sessionId: "claude-session",
source: "copilot",
})
},
)
const page = await fetchEventsPage()
const row = page.items[0]
expect(row.trace_id).toBe("trace-123")
expect(row.session_id).toBe("opencode-session")
expect(row.user_id).toBe("copilot-login")
expect(row.total_tokens).toBe(15)
})
test("uses explicit metadata session id when no session affinity exists", async () => {
recordTokenUsageEvent({
endpoint: "provider_messages",
input_tokens: 12,
model: "claude-test",
output_tokens: 4,
providerName: "anthropic",
sessionId: "claude-session",
source: "provider",
})
const page = await fetchEventsPage()
const row = page.items[0]
expect(typeof row.trace_id).toBe("string")
expect(row.trace_id.length).toBeGreaterThan(0)
expect(row.session_id).toBe("claude-session")
expect(row.user_id).toBe("anthropic")
expect(row.total_tokens).toBe(16)
})
test("does not write zero-token usage events", async () => {
recordTokenUsageEvent({
endpoint: "chat_completions",
input_tokens: 0,
model: "gpt-test",
output_tokens: 0,
source: "copilot",
})
const response = await createTokenUsageApp().request(
"/token-usage?period=day",
)
expect(response.status).toBe(200)
const summary = (await response.json()) as TokenUsageSummary
expect(summary.totals.request_count).toBe(0)
})
test("summarizes by model with total token and user fields", async () => {
recordTokenUsageEvent({
cache_creation_input_tokens: 1,
cache_read_input_tokens: 2,
endpoint: "chat_completions",
input_tokens: 10,
model: "gpt-a",
output_tokens: 3,
source: "copilot",
})
recordTokenUsageEvent({
cache_read_input_tokens: 4,
endpoint: "responses",
input_tokens: 20,
model: "gpt-b",
output_tokens: 6,
source: "copilot",
})
const response = await createTokenUsageApp().request(
"/token-usage?period=day",
)
expect(response.status).toBe(200)
const summary = (await response.json()) as TokenUsageSummary
expect(summary.totals).toEqual({
cache_creation_input_tokens: 1,
cache_read_input_tokens: 6,
input_tokens: 30,
output_tokens: 9,
request_count: 2,
total_tokens: 46,
})
expect(summary.totals.total_tokens).toBe(46)
expect(summary.byModel).toHaveLength(2)
expect(summary.byModel.every((row) => row.total_tokens > 0)).toBe(true)
})
test("returns paginated usage events with user id", async () => {
recordTokenUsageEvent({
endpoint: "chat_completions",
input_tokens: 10,
model: "gpt-a",
output_tokens: 2,
source: "copilot",
})
recordTokenUsageEvent({
endpoint: "provider_messages",
input_tokens: 20,
model: "claude-a",
output_tokens: 5,
providerName: "anthropic",
sessionId: "claude-session",
source: "provider",
traceId: "trace-provider",
})
const response = await createTokenUsageApp().request(
"/token-usage/events?period=day&page=1&page_size=1",
)
expect(response.status).toBe(200)
const page = (await response.json()) as TokenUsageEventsPage
expect(page.total).toBe(2)
expect(page.page).toBe(1)
expect(page.page_size).toBe(1)
expect(page.total_pages).toBe(2)
expect(page.items).toHaveLength(1)
expect(page.items[0]?.user_id).toBe("anthropic")
expect(page.items[0]?.trace_id).toBe("trace-provider")
expect(page.items[0]?.session_id).toBe("claude-session")
expect(page.items[0]?.total_tokens).toBe(25)
})
test("only falls back to interaction id when no real session id exists", async () => {
const recordWithFallback = createCopilotTokenUsageRecorder({
endpoint: "responses",
fallbackSessionId: "interaction-session",
model: "gpt-test",
})
const recordWithRealSession = createCopilotTokenUsageRecorder({
endpoint: "responses",
fallbackSessionId: "ignored-interaction-session",
model: "gpt-test",
sessionId: "real-session",
})
recordWithFallback({
input_tokens: 5,
})
recordWithRealSession({
input_tokens: 7,
})
const page = await fetchEventsPage(10)
expect(page.items).toHaveLength(2)
expect(page.items[0]?.session_id).toBe("real-session")
expect(page.items[1]?.session_id).toBe("interaction-session")
})
test("returns daily token usage buckets by model with total tokens", async () => {
setSystemTime(localDate(2026, 4, 8))
recordTokenUsageEvent({
endpoint: "chat_completions",
input_tokens: 999,
model: "outside-week",
output_tokens: 1,
source: "copilot",
})
setSystemTime(localDate(2026, 4, 12, 10))
recordTokenUsageEvent({
cache_creation_input_tokens: 1,
cache_read_input_tokens: 2,
endpoint: "chat_completions",
input_tokens: 10,
model: "gpt-a",
output_tokens: 3,
source: "copilot",
})
recordTokenUsageEvent({
cache_read_input_tokens: 4,
endpoint: "responses",
input_tokens: 20,
model: "gpt-b",
output_tokens: 5,
source: "copilot",
})
setSystemTime(localDate(2026, 4, 14, 9))
recordTokenUsageEvent({
endpoint: "messages",
input_tokens: 6,
model: "gpt-a",
output_tokens: 4,
source: "copilot",
total_tokens: 100,
})
setSystemTime(localDate(2026, 4, 15))
const response = await createTokenUsageApp().request(
"/token-usage/daily?period=week",
)
expect(response.status).toBe(200)
const daily = (await response.json()) as TokenUsageDailySummary
expect(daily.period).toBe("week")
expect(daily.days).toHaveLength(7)
expect(daily.totals).toEqual({
cache_creation_input_tokens: 1,
cache_read_input_tokens: 6,
input_tokens: 36,
output_tokens: 12,
request_count: 3,
total_tokens: 145,
})
expect(daily.byModel.map((model) => model.model)).toEqual([
"gpt-a",
"gpt-b",
])
expect(daily.byModel[0]?.total_tokens).toBe(116)
const firstDay = daily.days[0]
expect(firstDay?.date).toBe(localDateLabel(localDate(2026, 4, 9)))
expect(firstDay?.totals.total_tokens).toBe(0)
const may12 = daily.days.find(
(day) => day.date === localDateLabel(localDate(2026, 4, 12)),
)
expect(may12?.totals).toEqual({
cache_creation_input_tokens: 1,
cache_read_input_tokens: 6,
input_tokens: 30,
output_tokens: 8,
request_count: 2,
total_tokens: 45,
})
expect(may12?.byModel.map((model) => model.model)).toEqual([
"gpt-b",
"gpt-a",
])
const may14 = daily.days.find(
(day) => day.date === localDateLabel(localDate(2026, 4, 14)),
)
expect(may14?.totals.total_tokens).toBe(100)
expect(may14?.byModel[0]?.model).toBe("gpt-a")
expect(may14?.byModel[0]?.total_tokens).toBe(100)
})
test("returns empty daily buckets and falls back invalid period to day", async () => {
setSystemTime(localDate(2026, 4, 15))
const response = await createTokenUsageApp().request(
"/token-usage/daily?period=invalid",
)
expect(response.status).toBe(200)
const daily = (await response.json()) as TokenUsageDailySummary
expect(daily.period).toBe("day")
expect(daily.days).toHaveLength(1)
expect(daily.days[0]?.date).toBe(localDateLabel(localDate(2026, 4, 15)))
expect(daily.days[0]?.totals.total_tokens).toBe(0)
expect(daily.byModel).toEqual([])
expect(daily.totals.request_count).toBe(0)
})
})