fix(vercel-gateway): route network sources through aggregation

The synthetic source path has no on-disk file, so fingerprintFile returned
null and parseProviderSources dropped the source before parsing — the provider
always reported $0 even with a key set. Add a `network` provider flag; such
sources skip the fingerprint gate, re-fetch each run with a synthetic
fingerprint, and keep the gateway's reported cost (instead of recomputing from
tokens, which would be $0 for any model codeburn can't price). Adds an
end-to-end test through parseAllSessions.
This commit is contained in:
iamtoruk 2026-06-09 14:57:58 -07:00 committed by Resham Joshi
parent f4fd6a1640
commit e7c0e21846
4 changed files with 68 additions and 1 deletions

View file

@ -1696,7 +1696,7 @@ function providerCallToCachedCall(call: ParsedProviderCall): CachedCall {
webSearchRequests: call.webSearchRequests,
cacheCreationOneHourTokens: 0,
},
costUSD: (call.provider === 'mistral-vibe' || call.provider === 'antigravity' || call.provider === 'devin') ? call.costUSD : undefined,
costUSD: (call.provider === 'mistral-vibe' || call.provider === 'antigravity' || call.provider === 'devin' || call.provider === 'vercel-gateway') ? call.costUSD : undefined,
speed: call.speed,
timestamp: call.timestamp,
tools: call.tools,
@ -1931,6 +1931,16 @@ async function parseProviderSources(
for (const source of sources) {
allDiscoveredFiles.add(source.path)
// Network providers (e.g. Vercel AI Gateway) have no on-disk file — their data
// comes from a live API fetch in createSessionParser. There's nothing to
// fingerprint or incrementally cache, so re-fetch every run with a synthetic
// fingerprint (mtime=now so the date-range filter below never excludes it).
if (provider.network) {
changedSources.push({ source, fp: { dev: 0, ino: 0, mtimeMs: Date.now(), sizeBytes: 0 } })
continue
}
const fp = await fingerprintFile(source.path)
if (!fp) continue

View file

@ -38,6 +38,9 @@ export type ParsedProviderCall = {
export type Provider = {
name: string
displayName: string
// Data comes from a live API fetch (no on-disk file). Such sources can't be
// fingerprinted or incrementally cached, so the parser re-fetches every run.
network?: boolean
modelDisplayName(model: string): string
toolDisplayName(rawTool: string): string
discoverSessions(): Promise<SessionSource[]>

View file

@ -120,6 +120,7 @@ function createParser(
export const vercelGateway: Provider = {
name: 'vercel-gateway',
displayName: 'Vercel AI Gateway',
network: true,
modelDisplayName(model: string): string {
const slash = model.indexOf('/')

View file

@ -1,5 +1,9 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mkdtemp, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { fetchVercelGatewayReport, vercelGateway } from '../../src/providers/vercel-gateway.js'
import { parseAllSessions, clearSessionCache } from '../../src/parser.js'
describe('vercel-gateway provider', () => {
const originalFetch = globalThis.fetch
@ -62,3 +66,52 @@ describe('vercel-gateway provider', () => {
expect(calls[0]?.model).toBe('anthropic/claude-sonnet-4.6')
})
})
describe('vercel-gateway end-to-end (parseAllSessions network path)', () => {
const originalFetch = globalThis.fetch
const originalKey = process.env.AI_GATEWAY_API_KEY
const originalCacheDir = process.env.CODEBURN_CACHE_DIR
let cacheDir: string
beforeEach(async () => {
cacheDir = await mkdtemp(join(tmpdir(), 'cb-vercel-cache-'))
process.env.CODEBURN_CACHE_DIR = cacheDir
process.env.AI_GATEWAY_API_KEY = 'test-key'
clearSessionCache()
})
afterEach(async () => {
globalThis.fetch = originalFetch
if (originalKey === undefined) delete process.env.AI_GATEWAY_API_KEY
else process.env.AI_GATEWAY_API_KEY = originalKey
if (originalCacheDir === undefined) delete process.env.CODEBURN_CACHE_DIR
else process.env.CODEBURN_CACHE_DIR = originalCacheDir
clearSessionCache()
vi.restoreAllMocks()
await rm(cacheDir, { recursive: true, force: true })
})
// Regression: the synthetic source path `vercel-ai-gateway:report` has no file
// on disk, so it was dropped by the fingerprintFile gate in parseProviderSources
// and the provider always reported $0. Network providers must survive that gate
// and contribute their fetched cost through the real aggregation pipeline.
it('network source survives the fingerprint gate and contributes cost', async () => {
globalThis.fetch = vi.fn(async () => ({
ok: true,
json: async () => ({
results: [
{ day: '2026-06-01', model: 'openai/gpt-4o', total_cost: 12.34, input_tokens: 1000, output_tokens: 500, request_count: 3 },
],
}),
})) as typeof fetch
const range = {
start: new Date('2026-05-01T00:00:00.000Z'),
end: new Date('2026-06-09T23:59:59.999Z'),
}
const projects = await parseAllSessions(range, 'vercel-gateway')
const total = projects.reduce((sum, p) => sum + p.totalCostUSD, 0)
expect(total).toBeCloseTo(12.34, 2)
})
})