Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 38 additions & 0 deletions apps/evi/agent/lib/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,44 @@ describe('reportQuery', () => {
})
})

describe('reportWindowProblem', () => {
it('accepts a window that ends today', async () => {
const { reportWindowProblem } = await loadGateway({})
expect(reportWindowProblem('2026-10-01', '2026-10-08', '2026-10-08')).toBeUndefined()
})

it('rejects a window whose end date has not happened yet', async () => {
const { reportWindowProblem } = await loadGateway({})
expect(reportWindowProblem('2026-02-07', '2027-03-07', '2026-10-08')).toEqual({
field: 'endDate',
message: 'endDate is in the future: the window ends 2027-03-07 but today is 2026-10-08 (UTC), and the gateway has no data for dates that have not happened yet',
})
})

it('rejects an inverted window', async () => {
const { reportWindowProblem } = await loadGateway({})
expect(reportWindowProblem('2026-10-08', '2026-10-01', '2026-10-08')).toEqual({
field: 'startDate',
message: 'startDate must not be later than endDate',
})
})
})

describe('todayUtc', () => {
it('returns the UTC calendar date regardless of the local clock', async () => {
const { todayUtc } = await loadGateway({})
vi.useFakeTimers()
try {
vi.setSystemTime(new Date('2026-10-08T23:30:00Z'))
expect(todayUtc()).toBe('2026-10-08')
vi.setSystemTime(new Date('2026-10-09T00:30:00Z'))
expect(todayUtc()).toBe('2026-10-09')
} finally {
vi.useRealTimers()
}
})
})

describe('scopedReport', () => {
const payload = {
results: [
Expand Down
29 changes: 29 additions & 0 deletions apps/evi/agent/lib/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,35 @@ export function reportApiKeyName(): string | undefined {
return process.env.AI_GATEWAY_REPORT_API_KEY_NAME?.trim() || undefined
}

/**
* Today's date in UTC, `YYYY-MM-DD`: the reference spend-report windows are
* validated against.
*/
export function todayUtc(): string {
return new Date().toISOString().slice(0, 10)
}

/**
* Why a spend-report window is invalid, or undefined when it is fine. The
* gateway silently answers an out-of-range window with zero rows, and the
* empty-scope note then reads as a tagging gap, so the window is rejected up
* front instead.
*/
export function reportWindowProblem(
startDate: string,
endDate: string,
today = todayUtc(),
): { field: 'startDate' | 'endDate', message: string } | undefined {
if (startDate > endDate) return { field: 'startDate', message: 'startDate must not be later than endDate' }
if (endDate > today) {
return {
field: 'endDate',
message: `endDate is in the future: the window ends ${endDate} but today is ${today} (UTC), and the gateway has no data for dates that have not happened yet`,
}
}
return undefined
}

/** The caller-supplied part of a spend-report request. */
export interface ReportQueryInput {
groupBy?: string
Expand Down
8 changes: 4 additions & 4 deletions apps/evi/agent/tools/ai-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useLogger } from 'evlog/eve'
import { defineDynamic, defineTool } from 'eve/tools'
import { z } from 'zod'
import { eviErrors, refusal } from '../lib/errors'
import { defaultReportTag, gatewayFetch, reportQuery, scopedReport } from '../lib/gateway'
import { defaultReportTag, gatewayFetch, reportQuery, reportWindowProblem, scopedReport } from '../lib/gateway'
import { canAccessAdminTools } from '../lib/trust'

function notAvailable(tool: string) {
Expand Down Expand Up @@ -39,9 +39,9 @@ export default defineDynamic({
credentialType: z.enum(['byok', 'system']).optional(),
tags: z.array(z.string()).optional().describe('Override default report tags (env AI_GATEWAY_REPORT_TAGS / default evi:env:<environment>). Ignored while key-name scoping applies, i.e. AI_GATEWAY_REPORT_API_KEY_NAME is set and no groupBy was requested.'),
tagsMatch: z.enum(['any', 'all']).optional(),
}).refine(({ startDate, endDate }) => startDate <= endDate, {
message: 'startDate must not be later than endDate',
path: ['startDate'],
}).superRefine(({ startDate, endDate }, ctx) => {
const problem = reportWindowProblem(startDate, endDate)
if (problem) ctx.addIssue({ code: 'custom', message: problem.message, path: [problem.field] })
}),
async execute(input, toolCtx) {
if (!canAccessAdminTools(toolCtx.session.auth.current)) return notAvailable('ai_gateway__report')
Expand Down
Loading