mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-14 19:14:28 +00:00
fix(usage): per-provider snapshot capture, placeholder guard, path-id display fallback
This commit is contained in:
parent
9d5f745b5b
commit
b71ec34a21
6 changed files with 58 additions and 2 deletions
|
|
@ -699,6 +699,27 @@ describe('usage_snapshot telemetry props', () => {
|
|||
}
|
||||
}
|
||||
|
||||
it('caps providers at 8, sorted by cost descending, bucketed', () => {
|
||||
const p = enrichedPayload()
|
||||
p.current.providers = {
|
||||
claude: 500, codex: 40, gemini: 5, cursor: 0.5, antigravity: 300,
|
||||
copilot: 20, windsurf: 2, amp: 0.1, cline: 0.02,
|
||||
}
|
||||
const props = sanitizeProps(usageSnapshotProps(p))
|
||||
const providers = props.providers as Array<{ name: string; costBucket: string }>
|
||||
expect(providers).toHaveLength(8)
|
||||
expect(providers).toEqual([
|
||||
{ name: 'claude', costBucket: '200-1k' },
|
||||
{ name: 'antigravity', costBucket: '200-1k' },
|
||||
{ name: 'codex', costBucket: '10-50' },
|
||||
{ name: 'copilot', costBucket: '10-50' },
|
||||
{ name: 'gemini', costBucket: '1-10' },
|
||||
{ name: 'windsurf', costBucket: '1-10' },
|
||||
{ name: 'cursor', costBucket: '<1' },
|
||||
{ name: 'amp', costBucket: '<1' },
|
||||
])
|
||||
})
|
||||
|
||||
it('crosses each top model with its dominant task category when the report joins', () => {
|
||||
// The overview model name is the display/short name; for Claude that equals
|
||||
// modelDisplayName, which is how the by-model report row joins back.
|
||||
|
|
|
|||
|
|
@ -78,6 +78,12 @@ export function usageSnapshotProps(payload: MenubarPayload, modelCategories?: Ma
|
|||
if (topCategory) entry.topCategory = topCategory
|
||||
return entry
|
||||
}),
|
||||
// Per-provider spend, same cost-bucketing as models. `providers` maps lowercased
|
||||
// display name -> cost USD; sort by cost so the top spenders survive the cap.
|
||||
providers: Object.entries(payload.current.providers ?? {})
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 8)
|
||||
.map(([name, cost]) => ({ name, costBucket: costBucket(cost) })),
|
||||
// Aggregate task categories (the "purpose" dimension across all models).
|
||||
categories: (payload.current.topActivities ?? []).slice(0, 12).map(activity => ({
|
||||
name: activity.name,
|
||||
|
|
|
|||
|
|
@ -920,5 +920,9 @@ export function getShortModelName(model: string): string {
|
|||
// fall through to its raw id rather than show a wrong name/tier.
|
||||
if (canonical === key || canonical.startsWith(key + '-')) return name
|
||||
}
|
||||
// getCanonicalName only strips the leading provider prefix, so a raw
|
||||
// path-style id (e.g. fireworks/routers/glm-fast-latest) still has slashes
|
||||
// here. Fall back to the last path segment rather than showing the path.
|
||||
if (canonical.includes('/')) return canonical.slice(canonical.lastIndexOf('/') + 1)
|
||||
return canonical
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,6 +260,18 @@ function parseAntigravityServerCandidates(lines: string[]): ServerCandidate[] {
|
|||
.filter((server): server is ServerCandidate => server !== null)
|
||||
}
|
||||
|
||||
// Antigravity's own model-map config sometimes hasn't caught up with a new
|
||||
// model yet, so both the config key and displayName can still be the raw
|
||||
// placeholder id (e.g. "MODEL_PLACEHOLDER_M26"). Falling through to that
|
||||
// value as the "canonical" model would leak an internal placeholder as a
|
||||
// model name; 'unknown' is what this file already uses when no model can be
|
||||
// resolved at all (see antigravitySqliteModel).
|
||||
const MODEL_PLACEHOLDER_PATTERN = /^MODEL_PLACEHOLDER_/
|
||||
|
||||
function dropPlaceholderModelId(model: string): string {
|
||||
return MODEL_PLACEHOLDER_PATTERN.test(model) ? 'unknown' : model
|
||||
}
|
||||
|
||||
function getCanonicalModelId(key: string, displayName?: string): string {
|
||||
if (displayName) {
|
||||
const lower = displayName.toLowerCase()
|
||||
|
|
@ -286,7 +298,7 @@ function getCanonicalModelId(key: string, displayName?: string): string {
|
|||
return 'gemini-3-pro'
|
||||
}
|
||||
}
|
||||
return key
|
||||
return dropPlaceholderModelId(key)
|
||||
}
|
||||
|
||||
export function extractAntigravityModelMap(resp: unknown): ModelMap {
|
||||
|
|
@ -880,7 +892,7 @@ function buildCallsFromGeneratorMetadata(
|
|||
const responseId = usage.responseId || String(i)
|
||||
const dedupKey = `antigravity:${cascadeId}:${responseId}`
|
||||
|
||||
const model = modelMap[usage.model] ?? usage.model
|
||||
const model = dropPlaceholderModelId(modelMap[usage.model] ?? usage.model)
|
||||
const pricingModel = normalizePricingModel(model)
|
||||
const timestamp = entry.chatModel?.chatStartMetadata?.createdAt ?? ''
|
||||
const costUSD = calculateCost(pricingModel, inputTokens, responseTokens + thinkingTokens, 0, 0, 0)
|
||||
|
|
|
|||
|
|
@ -117,6 +117,11 @@ describe('getShortModelName', () => {
|
|||
// grok-composer has no alias, just a missing display entry.
|
||||
expect(getShortModelName('grok-composer-2.5-fast')).toBe('Grok Composer 2.5 Fast')
|
||||
})
|
||||
|
||||
it('shows the last path segment for an unmapped path-style raw id', () => {
|
||||
expect(getShortModelName('fireworks/models/kimi-k2p7-code')).toBe('kimi-k2p7-code')
|
||||
expect(getShortModelName('fireworks/routers/glm-fast-latest')).toBe('glm-fast-latest')
|
||||
})
|
||||
})
|
||||
|
||||
describe('claude-fable-5 pricing + name', () => {
|
||||
|
|
|
|||
|
|
@ -187,6 +187,14 @@ describe('antigravity provider helpers', () => {
|
|||
expect(extractAntigravityModelMap(null)).toEqual({})
|
||||
})
|
||||
|
||||
it('never leaks a raw MODEL_PLACEHOLDER id as the canonical model name', () => {
|
||||
// The config key itself is still the unresolved placeholder (Antigravity
|
||||
// hasn't shipped a friendly key/displayName for this model yet).
|
||||
expect(extractAntigravityModelMap({
|
||||
models: { MODEL_PLACEHOLDER_M26: { model: 'MODEL_PLACEHOLDER_M26' } },
|
||||
})).toEqual({ MODEL_PLACEHOLDER_M26: 'unknown' })
|
||||
})
|
||||
|
||||
it('extracts generator metadata from wrapped and unwrapped RPC responses', () => {
|
||||
const metadata = [{
|
||||
chatModel: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue