mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-16 19:44:14 +00:00
fix: pricing accuracy, stream leak, CSV injection hardening
- Remove bidirectional fuzzy match in getModelCosts that could return wrong pricing when a short canonical name prefix-matched a longer key - Use explicit undefined check in parseLiteLLMEntry so free models with zero cost are not silently dropped from the LiteLLM pricing database - Destroy read stream in finally block of readSessionLines to prevent file descriptor leaks when the generator is abandoned early - Extend CSV injection escaping to cover tab and carriage-return prefixes - Add optional chaining fallback for empty periods in exportCsv/exportJson - Add regression tests for all fixes (models, export, fs-utils)
This commit is contained in:
parent
b61e7cd32a
commit
a4d261a536
6 changed files with 117 additions and 6 deletions
|
|
@ -6,7 +6,7 @@ import { getCurrency, convertCost } from './currency.js'
|
|||
import { dateKey } from './day-aggregator.js'
|
||||
|
||||
function escCsv(s: string): string {
|
||||
const sanitized = /^[=+\-@]/.test(s) ? `'${s}` : s
|
||||
const sanitized = /^[\t\r=+\-@]/.test(s) ? `'${s}` : s
|
||||
if (sanitized.includes(',') || sanitized.includes('"') || sanitized.includes('\n')) {
|
||||
return `"${sanitized.replace(/"/g, '""')}"`
|
||||
}
|
||||
|
|
@ -283,7 +283,7 @@ async function clearCodeburnExportFolder(path: string): Promise<void> {
|
|||
/// wipe a sensitive file (prior versions did `rm(path, { force: true })` unconditionally).
|
||||
export async function exportCsv(periods: PeriodExport[], outputPath: string): Promise<string> {
|
||||
const thirtyDays = periods.find(p => p.label === '30 Days')
|
||||
const thirtyDayProjects = thirtyDays?.projects ?? periods[periods.length - 1].projects
|
||||
const thirtyDayProjects = thirtyDays?.projects ?? periods[periods.length - 1]?.projects ?? []
|
||||
|
||||
let folder = resolve(outputPath)
|
||||
if (folder.toLowerCase().endsWith('.csv')) {
|
||||
|
|
@ -325,7 +325,7 @@ export async function exportCsv(periods: PeriodExport[], outputPath: string): Pr
|
|||
|
||||
export async function exportJson(periods: PeriodExport[], outputPath: string): Promise<string> {
|
||||
const thirtyDays = periods.find(p => p.label === '30 Days')
|
||||
const thirtyDayProjects = thirtyDays?.projects ?? periods[periods.length - 1].projects
|
||||
const thirtyDayProjects = thirtyDays?.projects ?? periods[periods.length - 1]?.projects ?? []
|
||||
const { code, rate, symbol } = getCurrency()
|
||||
|
||||
const data = {
|
||||
|
|
|
|||
|
|
@ -89,5 +89,7 @@ export async function* readSessionLines(filePath: string): AsyncGenerator<string
|
|||
for await (const line of rl) yield line
|
||||
} catch (err) {
|
||||
warn(`stream read failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
|
||||
} finally {
|
||||
stream.destroy()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ function getCachePath(): string {
|
|||
}
|
||||
|
||||
function parseLiteLLMEntry(entry: LiteLLMEntry): ModelCosts | null {
|
||||
if (!entry.input_cost_per_token || !entry.output_cost_per_token) return null
|
||||
if (entry.input_cost_per_token === undefined || entry.output_cost_per_token === undefined) return null
|
||||
return {
|
||||
inputCostPerToken: entry.input_cost_per_token,
|
||||
outputCostPerToken: entry.output_cost_per_token,
|
||||
|
|
@ -140,7 +140,7 @@ export function getModelCosts(model: string): ModelCosts | null {
|
|||
}
|
||||
|
||||
for (const [key, costs] of pricingCache ?? new Map()) {
|
||||
if (canonical.startsWith(key) || key.startsWith(canonical)) return costs
|
||||
if (canonical.startsWith(key)) return costs
|
||||
}
|
||||
|
||||
for (const [key, costs] of Object.entries(FALLBACK_PRICING)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue