refactor(pricing): fan-out batch 3 — hermes, kiro, codewhale

This commit is contained in:
iamtoruk 2026-07-26 08:29:10 -07:00
parent 7bc5f3b044
commit c56f872cf3
6 changed files with 62 additions and 73 deletions

View file

@ -4,7 +4,7 @@ import { join } from 'path'
import { extractBashCommands } from '../bash-utils.js'
import { readSessionFile } from '../fs-utils.js'
import { calculateCost, getShortModelName } from '../models.js'
import { getShortModelName } from '../models.js'
import type { ToolCall } from '../types.js'
import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js'
@ -390,10 +390,9 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
const totalTokens = safeTokenCount(metadata.total_tokens)
const model = metadata.model ?? metadata.model_provider ?? 'unknown'
const localCost = reportedCost(metadata.cost)
const costUSD = localCost.exact
? localCost.value
: calculateCost(model, totalTokens, 0, 0, 0, 0)
if (totalTokens === 0 && costUSD === 0) return
// Match the pre-lift guard exactly: it skipped zero-token sessions whose
// COMPUTED cost was 0 — which included an exact recorded cost of 0.
if (totalTokens === 0 && (!localCost.exact || localCost.value === 0)) return
const deduplicationKey = `codewhale:${metadata.id}`
if (seenKeys.has(deduplicationKey)) return
@ -420,7 +419,9 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests,
costUSD,
...(localCost.exact
? { costUSD: localCost.value, costBasis: 'measured' as const }
: { costBasis: 'estimated' as const }),
costIsEstimated: !localCost.exact,
tools,
bashCommands,

View file

@ -2,7 +2,7 @@ import { readdir, stat } from 'fs/promises'
import { basename, dirname, join } from 'path'
import { homedir } from 'os'
import { calculateCost, getShortModelName } from '../models.js'
import { getShortModelName } from '../models.js'
import { isSqliteAvailable, getSqliteLoadError, openDatabase, isSqliteBusyError, type SqliteDatabase } from '../sqlite.js'
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
import type { ToolCall } from '../types.js'
@ -390,23 +390,12 @@ function createParser(source: SessionSource, seenKeys: Set<string>, hermesHome:
seenKeys.add(dedupKey)
// Hermes bills reasoning tokens at the output rate (same as Gemini).
// The LiteLLM model table is used as a fallback when Hermes has not
// stored an actual or estimated cost for the session.
const calculatedCost = calculateCost(
model,
inputTokens,
outputTokens + reasoningTokens,
cacheWriteTokens,
cacheReadTokens,
0,
)
// When Hermes stored an actual or estimated cost, pass it as measured;
// otherwise the pricing pass will estimate from token buckets.
const recordedCost =
(row.actual_cost_usd ?? 0) > 0 ? row.actual_cost_usd!
: (row.estimated_cost_usd ?? 0) > 0 ? row.estimated_cost_usd!
: null
// When Hermes stored no cost (e.g. subscription-billed sessions), the
// figure is our LiteLLM-priced estimate from the session token totals.
const costUSD = recordedCost ?? calculatedCost
const costIsEstimated = recordedCost === null
result = {
@ -419,7 +408,9 @@ function createParser(source: SessionSource, seenKeys: Set<string>, hermesHome:
cachedInputTokens: cacheReadTokens,
reasoningTokens,
webSearchRequests: 0,
costUSD,
...(recordedCost !== null
? { costUSD: recordedCost, costBasis: 'measured' as const }
: { costBasis: 'estimated' as const }),
costIsEstimated,
tools,
bashCommands,

View file

@ -5,7 +5,6 @@ import { basename, dirname, extname, join } from 'path'
import { homedir } from 'os'
import { readSessionFile } from '../fs-utils.js'
import { calculateCost } from '../models.js'
import { estimateTokensFromChars } from '../token-estimate.js'
import type { ToolCall } from '../types.js'
import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js'
@ -227,7 +226,6 @@ function parseChatFile(data: KiroChatFile, sessionId: string, project: string, s
const outputTokens = estimateTokensFromChars(totalOutputChars)
const inputTokens = estimateTokensFromChars(pendingUserMessage.length)
const costUSD = calculateCost(modelId, inputTokens, outputTokens, 0, 0, 0)
const tsDate = parseKiroTimestamp(metadata.startTime)
if (!tsDate) return results
const timestamp = tsDate.toISOString()
@ -243,7 +241,7 @@ function parseChatFile(data: KiroChatFile, sessionId: string, project: string, s
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
costUSD,
costBasis: 'estimated',
costIsEstimated: true,
tools: [...new Set(allTools)],
bashCommands: [],
@ -377,9 +375,6 @@ function parseModernExecution(data: KiroModernExecution, sourcePath: string, see
// Prefer real metered credits at the public overage rate; fall back to
// token-estimated pricing when the execution has no usage data — same
// contract as the CLI and v2 parsers.
const costUSD = executionCredits > 0
? executionCredits * USD_PER_KIRO_CREDIT
: calculateCost(modelId, inputTokens, outputTokens, 0, 0, 0)
seenKeys.add(dedupKey)
results.push({
@ -392,7 +387,9 @@ function parseModernExecution(data: KiroModernExecution, sourcePath: string, see
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
costUSD,
...(executionCredits > 0
? { costUSD: executionCredits * USD_PER_KIRO_CREDIT, costBasis: 'measured' as const }
: { costBasis: 'estimated' as const }),
costIsEstimated: executionCredits === 0,
tools: [...new Set(allTools)],
bashCommands: [],
@ -473,9 +470,6 @@ function parseCliSession(meta: KiroCliSessionMeta, entries: KiroCliEntry[], seen
const turnCredits = turnMeta?.metering_usage
? turnMeta.metering_usage.reduce((sum, m) => sum + m.value, 0)
: 0
const costUSD = turnCredits > 0
? turnCredits * USD_PER_KIRO_CREDIT
: calculateCost(modelId, inputTokens, outputTokens, 0, 0, 0)
seenKeys.add(dedupKey)
results.push({
@ -488,7 +482,9 @@ function parseCliSession(meta: KiroCliSessionMeta, entries: KiroCliEntry[], seen
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
costUSD,
...(turnCredits > 0
? { costUSD: turnCredits * USD_PER_KIRO_CREDIT, costBasis: 'measured' as const }
: { costBasis: 'estimated' as const }),
costIsEstimated: turnCredits === 0,
tools: [...new Set(allTools)],
bashCommands: [],
@ -633,7 +629,6 @@ async function parseWorkspaceSession(record: Record<string, unknown>, source: Se
const inputTokens = estimateTokensFromChars(inputChars)
const outputTokens = estimateTokensFromChars(outputChars)
const costUSD = calculateCost(modelId, inputTokens, outputTokens, 0, 0, 0)
results.push({
provider: 'kiro',
@ -645,7 +640,7 @@ async function parseWorkspaceSession(record: Record<string, unknown>, source: Se
cachedInputTokens: 0,
reasoningTokens: 0,
webSearchRequests: 0,
costUSD,
costBasis: 'estimated',
costIsEstimated: true,
tools: [...new Set(allTools)],
bashCommands: [],
@ -743,9 +738,6 @@ async function parseV2Session(source: SessionSource, seenKeys: Set<string>): Pro
// no usage_summary (e.g. still in progress or null usage). Reasoning
// text is billed as output, so combine it for pricing only (same as
// the codex provider).
const costUSD = turnCredits > 0
? turnCredits * USD_PER_KIRO_CREDIT
: calculateCost(modelId, inputTokens, outputTokens + reasoningTokens, 0, 0, 0)
results.push({
provider: 'kiro',
model: modelId,
@ -756,7 +748,9 @@ async function parseV2Session(source: SessionSource, seenKeys: Set<string>): Pro
cachedInputTokens: 0,
reasoningTokens,
webSearchRequests: 0,
costUSD,
...(turnCredits > 0
? { costUSD: turnCredits * USD_PER_KIRO_CREDIT, costBasis: 'measured' as const }
: { costBasis: 'estimated' as const }),
costIsEstimated: turnCredits === 0,
tools: [...new Set(tools)],
bashCommands: [],

View file

@ -4,6 +4,7 @@ import { tmpdir } from 'os'
import { join } from 'path'
import { clearSessionCache, parseAllSessions } from '../../src/parser.js'
import { priceProviderCall } from '../../src/pricing-pass.js'
import { sessionCachePath } from '../../src/session-cache.js'
import { MAX_SESSION_FILE_BYTES } from '../../src/fs-utils.js'
import { codewhale, createCodeWhaleProvider } from '../../src/providers/codewhale.js'
@ -64,7 +65,7 @@ async function parseOne(path: string, seenKeys = new Set<string>()): Promise<Par
const source = { path, project: 'fixture', provider: 'codewhale' }
const calls: ParsedProviderCall[] = []
for await (const call of codewhale.createSessionParser(source, seenKeys).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
return calls
}

View file

@ -5,6 +5,7 @@ import { createRequire } from 'node:module'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { calculateCost } from '../../src/models.js'
import { priceProviderCall } from '../../src/pricing-pass.js'
import { createHermesProvider } from '../../src/providers/hermes.js'
import { isSqliteAvailable } from '../../src/sqlite.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
@ -190,7 +191,7 @@ async function collectCalls(hermesHome: string, sourcePath: string): Promise<Par
const provider = createHermesProvider(hermesHome)
const calls: ParsedProviderCall[] = []
for await (const call of provider.createSessionParser({ path: sourcePath, project: 'hermes', provider: 'hermes' }, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
return calls
}

View file

@ -3,6 +3,7 @@ import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises'
import { join } from 'path'
import { tmpdir, homedir } from 'os'
import { priceProviderCall } from '../../src/pricing-pass.js'
import { kiro, createKiroProvider } from '../../src/providers/kiro.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'
@ -99,7 +100,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: chatPath, project: 'myproject', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
const call = calls[0]!
@ -123,7 +124,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: chatPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('kiro-auto')
@ -158,7 +159,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: chatPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(0)
})
@ -174,10 +175,10 @@ describe('kiro provider - chat file parsing', () => {
const seenKeys = new Set<string>()
const calls1: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, seenKeys).parse()) calls1.push(call)
for await (const call of kiro.createSessionParser(source, seenKeys).parse()) calls1.push(priceProviderCall(call))
const calls2: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, seenKeys).parse()) calls2.push(call)
for await (const call of kiro.createSessionParser(source, seenKeys).parse()) calls2.push(priceProviderCall(call))
expect(calls1).toHaveLength(1)
expect(calls2).toHaveLength(0)
@ -186,7 +187,7 @@ describe('kiro provider - chat file parsing', () => {
it('returns empty for missing file', async () => {
const source = { path: '/nonexistent/test.chat', project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(0)
})
@ -199,7 +200,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: chatPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(0)
})
@ -213,7 +214,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: chatPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.outputTokens).toBe(109)
@ -231,7 +232,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: chatPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('claude-haiku-4-5')
@ -250,7 +251,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: chatPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.sessionId).toBe('my-workflow-id')
@ -272,7 +273,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: executionPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
const call = calls[0]!
@ -303,7 +304,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: indexPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(0)
})
@ -325,7 +326,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: executionPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('kiro-auto')
@ -345,7 +346,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: executionPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.timestamp).toBe('2026-04-27T23:36:40.000Z')
@ -363,7 +364,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: executionPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.timestamp).toBe('2026-04-27T23:36:40.000Z')
@ -421,7 +422,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: executionPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.userMessage).toBe(`request from ${key}`)
@ -448,7 +449,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: executionPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.tools).toEqual(['Bash'])
@ -474,7 +475,7 @@ describe('kiro provider - chat file parsing', () => {
const source = { path: executionPath, project: 'test', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.tools).toEqual(['Edit'])
@ -677,7 +678,7 @@ describe('kiro provider - CLI session discovery', () => {
const source = { path: join(cliDir, `${sessionId}.jsonl`), project: 'test-project', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
const call = calls[0]!
@ -719,7 +720,7 @@ describe('kiro provider - CLI session discovery', () => {
const source = { path: join(cliDir, `${sessionId}.jsonl`), project: 'multi', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(2)
expect(calls[0]!.userMessage).toBe('first question')
@ -749,7 +750,7 @@ describe('kiro provider - CLI session discovery', () => {
const source = { path: join(cliDir, `${sessionId}.jsonl`), project: 'test-project', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
// Token-priced at the session's real model, not $0 — same fallback
@ -783,7 +784,7 @@ describe('kiro provider - CLI session discovery', () => {
const source = { path: join(cliDir, `${sessionId}.jsonl`), project: 'test-project', provider: 'kiro' }
const calls: ParsedProviderCall[] = []
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of kiro.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
expect(calls).toHaveLength(1)
expect(calls[0]!.costIsEstimated).toBe(true)
@ -839,7 +840,7 @@ describe('kiro provider - context.messages with entries', () => {
const calls: ParsedProviderCall[] = []
for (const source of sessions) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
}
@ -879,7 +880,7 @@ describe('kiro provider - context.messages with entries', () => {
const calls: ParsedProviderCall[] = []
for (const source of sessions) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
}
@ -919,7 +920,7 @@ describe('kiro provider - context.messages with entries', () => {
const calls: ParsedProviderCall[] = []
for (const source of sessions) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
}
@ -947,7 +948,7 @@ describe('kiro provider - context.messages with entries', () => {
const calls: ParsedProviderCall[] = []
for (const source of sessions) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
}
@ -996,7 +997,7 @@ describe('kiro provider - workspace-sessions format', () => {
const calls: ParsedProviderCall[] = []
for (const source of wsSessions) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
}
@ -1032,7 +1033,7 @@ describe('kiro provider - workspace-sessions format', () => {
const calls: ParsedProviderCall[] = []
for (const source of sessions) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) {
calls.push(call)
calls.push(priceProviderCall(call))
}
}
@ -1139,7 +1140,7 @@ describe('kiro provider - v2 sess_ format', () => {
const calls: ParsedProviderCall[] = []
for (const source of v2) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(2)
@ -1173,7 +1174,7 @@ describe('kiro provider - v2 sess_ format', () => {
const sources = await provider.discoverSessions()
const calls: ParsedProviderCall[] = []
for (const source of sources) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(2)
@ -1199,7 +1200,7 @@ describe('kiro provider - v2 sess_ format', () => {
const sources = await provider.discoverSessions()
const calls: ParsedProviderCall[] = []
for (const source of sources) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(2)
@ -1218,7 +1219,7 @@ describe('kiro provider - v2 sess_ format', () => {
const sources = await provider.discoverSessions()
const calls: ParsedProviderCall[] = []
for (const source of sources) {
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(call)
for await (const call of provider.createSessionParser(source, new Set()).parse()) calls.push(priceProviderCall(call))
}
expect(calls).toHaveLength(1)
@ -1325,7 +1326,7 @@ describe('kiro provider - mixed-format coexistence (legacy + v1 + workspace-sess
const seenKeys = new Set<string>()
const calls: ParsedProviderCall[] = []
for (const source of sources) {
for await (const call of provider.createSessionParser(source, seenKeys).parse()) calls.push(call)
for await (const call of provider.createSessionParser(source, seenKeys).parse()) calls.push(priceProviderCall(call))
}
return calls
}