fix(usage): per-provider snapshot capture, placeholder guard, path-id display fallback

This commit is contained in:
iamtoruk 2026-07-18 03:37:02 -07:00
parent 9d5f745b5b
commit b71ec34a21
6 changed files with 58 additions and 2 deletions

View file

@ -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.

View file

@ -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,

View file

@ -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
}

View file

@ -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)

View file

@ -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', () => {

View file

@ -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: {