mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-20 22:14:36 +00:00
fix(providers): guard two malformed-input crashes in the parse path
- vscode-cline-parser: entry.ts was truthy-checked but not validity-checked, so a garbage timestamp made new Date(ts).toISOString() throw RangeError and abort the whole session parse. Validate the date, fall back to empty. - models: parseLiteLLMEntry read fields off its argument with no null/type guard, so a null value in the remote LiteLLM pricing JSON threw and aborted the entire live pricing load. Return null for a null/non-object entry. Both mutation-checked: the tests raise RangeError / TypeError before the fix.
This commit is contained in:
parent
2b49608fd2
commit
48fd0daa0c
4 changed files with 52 additions and 2 deletions
|
|
@ -164,7 +164,11 @@ function safePerTokenRate(n: number | undefined): number | null {
|
|||
return n
|
||||
}
|
||||
|
||||
function parseLiteLLMEntry(entry: LiteLLMEntry): ModelCosts | null {
|
||||
export function parseLiteLLMEntry(entry: LiteLLMEntry): ModelCosts | null {
|
||||
// The live LiteLLM map is remote JSON; a null (or non-object) value for a
|
||||
// model would make the field reads below throw and abort the whole pricing
|
||||
// load. Treat it as unparseable, like any other bad entry.
|
||||
if (!entry || typeof entry !== 'object') return null
|
||||
const inputCost = safePerTokenRate(entry.input_cost_per_token)
|
||||
const outputCost = safePerTokenRate(entry.output_cost_per_token)
|
||||
if (inputCost === null || outputCost === null) return null
|
||||
|
|
|
|||
|
|
@ -192,7 +192,11 @@ export function createClineParser(source: SessionSource, seenKeys: Set<string>,
|
|||
|
||||
if (tokensIn === 0 && tokensOut === 0) continue
|
||||
|
||||
const timestamp = entry.ts ? new Date(entry.ts).toISOString() : ''
|
||||
// entry.ts is truthy-checked but not validity-checked: a malformed
|
||||
// ts (garbage string, out-of-range number) makes new Date().toISOString()
|
||||
// throw RangeError, which would abort the whole session's parse. Guard it.
|
||||
const tsDate = entry.ts ? new Date(entry.ts) : null
|
||||
const timestamp = tsDate && !Number.isNaN(tsDate.getTime()) ? tsDate.toISOString() : ''
|
||||
const costUSD = cost ?? calculateCost(model, tokensIn, tokensOut, cacheWrites, cacheReads, 0)
|
||||
|
||||
yield {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
setLocalModelSavings,
|
||||
getLocalModelSavingsConfigHash,
|
||||
getPriceOverridesConfigHash,
|
||||
parseLiteLLMEntry,
|
||||
} from '../src/models.js'
|
||||
import { getDailyCacheConfigHash } from '../src/usage-aggregator.js'
|
||||
|
||||
|
|
@ -865,3 +866,18 @@ describe('findUnpricedModels', () => {
|
|||
expect(unpriced.map(u => u.model)).toEqual(['zz-big', 'zz-small'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseLiteLLMEntry hardening', () => {
|
||||
it('returns null instead of throwing on a null or non-object entry', () => {
|
||||
// The live LiteLLM map is remote JSON; a null value for a model used to
|
||||
// throw on the field reads and abort the whole pricing load.
|
||||
expect(parseLiteLLMEntry(null as unknown as Parameters<typeof parseLiteLLMEntry>[0])).toBeNull()
|
||||
expect(parseLiteLLMEntry(undefined as unknown as Parameters<typeof parseLiteLLMEntry>[0])).toBeNull()
|
||||
expect(parseLiteLLMEntry(42 as unknown as Parameters<typeof parseLiteLLMEntry>[0])).toBeNull()
|
||||
})
|
||||
|
||||
it('still parses a valid entry', () => {
|
||||
const costs = parseLiteLLMEntry({ input_cost_per_token: 0.000003, output_cost_per_token: 0.000015 } as Parameters<typeof parseLiteLLMEntry>[0])
|
||||
expect(costs).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -56,3 +56,29 @@ describe('VS Code Cline-family storage discovery', () => {
|
|||
].sort())
|
||||
})
|
||||
})
|
||||
|
||||
import { createClineParser } from '../../src/providers/vscode-cline-parser.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
|
||||
describe('VS Code Cline-family parse hardening', () => {
|
||||
it('yields with an empty timestamp instead of throwing on a malformed ts', async () => {
|
||||
// entry.ts is only truthy-checked; a garbage value made new Date(ts)
|
||||
// .toISOString() throw RangeError and abort the whole session parse.
|
||||
const taskDir = join(tmpDir, 'tasks', 'bad-ts')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), JSON.stringify([
|
||||
{ type: 'say', say: 'api_req_started', text: JSON.stringify({ tokensIn: 100, tokensOut: 50 }), ts: 'not-a-real-timestamp' },
|
||||
]))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), JSON.stringify([
|
||||
{ role: 'user', content: [{ type: 'text', text: 'hi\n<environment_details>\n</environment_details>' }] },
|
||||
]))
|
||||
|
||||
const source = { path: taskDir, project: 'p', provider: 'cline' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of createClineParser(source, new Set(), 'cline').parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.timestamp).toBe('')
|
||||
expect(calls[0]!.inputTokens).toBe(100)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue