Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions src/cli/analyzeSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@ const PRICE_PER_MTOK: Record<string, [number, number, number, number]> = {
haiku: [1, 5, 0.1, 1.25],
};

// ponytail: local stopgap for short ids (claude-sonnet-5) while upstream PR #235 is unmerged
export function priceFamily(model: string): string | null {
const info = parseModelString(model);
if (info) return info.family;
const m = /claude-(opus|sonnet|haiku)/.exec(model.toLowerCase());
return m ? m[1] : null;
}

export type BillingScheme = 'anthropic-style' | 'router-style' | 'no-cache' | 'mixed';

// anthropic-style billing has cache_write > 0 on rounds; routers typically report
Expand Down Expand Up @@ -157,7 +149,7 @@ function thinkingTokensOf(msg: ParsedMessage): number {

// cost of one round at the built-in claude price table; null = unpriced model
export function roundCostUsd(r: RoundRow): number | null {
const price = PRICE_PER_MTOK[priceFamily(r.model) ?? ''];
const price = PRICE_PER_MTOK[parseModelString(r.model)?.family ?? ''];
if (!price) return null;
return (
(r.inputTokens * price[0] +
Expand Down
11 changes: 5 additions & 6 deletions src/shared/utils/modelParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const KNOWN_FAMILIES: KnownModelFamily[] = ['sonnet', 'opus', 'haiku'];
*
* Supported formats:
* - New format: claude-{family}-{major}-{minor}-{date} (e.g., "claude-sonnet-4-5-20250929")
* - New format short: claude-{family}-{major} (e.g., "claude-sonnet-5")
* - Old format: claude-{major}-{family}-{date} (e.g., "claude-3-opus-20240229")
* - Old format with minor: claude-{major}-{minor}-{family}-{date} (e.g., "claude-3-5-sonnet-20241022")
*/
Expand Down Expand Up @@ -87,14 +88,12 @@ export function parseModelString(model: string | undefined): ModelInfo | null {

// Determine format based on family position
if (familyIndex === 1) {
// New format: claude-{family}-{major}-{minor}-{date}
// New format: claude-{family}-{major}[-{minor}][-{date}]
// e.g., claude-sonnet-4-5-20250929 -> ["claude", "sonnet", "4", "5", "20250929"]
if (parts.length < 4) {
return null;
}

// e.g., claude-sonnet-5 -> ["claude", "sonnet", "5"]
majorVersion = parseInt(parts[2], 10);
Comment thread
axisrow marked this conversation as resolved.
if (isNaN(majorVersion)) {
// 8 digits in major position is a misplaced date (claude-sonnet-20250929), not a version
if (isNaN(majorVersion) || /^\d{8}$/.test(parts[2])) {
return null;
}

Expand Down
9 changes: 1 addition & 8 deletions test/main/cli/analyzeSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
filterLedgerByDate,
normalizeCallKey,
parseArgs,
priceFamily,
} from '../../../src/cli/analyzeSession';
import { mapWithConcurrency, scanSessionFile } from '../../../src/cli/sessionInventory';
import { estimateTokens } from '../../../src/shared/utils/tokenFormatting';
Expand Down Expand Up @@ -327,13 +326,7 @@ describe('mapWithConcurrency', () => {
});
});

describe('priceFamily and billing scheme', () => {
it('extracts claude family from short ids without date', () => {
expect(priceFamily('claude-sonnet-5')).toBe('sonnet');
expect(priceFamily('claude-sonnet-5-20250929')).toBe('sonnet');
expect(priceFamily('glm-5.3-flash')).toBeNull();
});

describe('billing scheme', () => {
it('detects billing scheme from round signatures', () => {
const w = { cacheReadTokens: 0, cacheCreationTokens: 100 };
const r = { cacheReadTokens: 500, cacheCreationTokens: 0 };
Expand Down
18 changes: 18 additions & 0 deletions test/shared/utils/modelParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,24 @@ describe('modelParser', () => {
});
});

it('should parse short format without minor or date: claude-sonnet-5', () => {
const result = parseModelString('claude-sonnet-5');
expect(result).toEqual({
name: 'sonnet5',
family: 'sonnet',
majorVersion: 5,
minorVersion: null,
});
});

it('should return null for invalid short format with non-numeric version', () => {
expect(parseModelString('claude-sonnet-x')).toBeNull();
});

it('should return null for date-shaped major version: claude-sonnet-20250929', () => {
expect(parseModelString('claude-sonnet-20250929')).toBeNull();
});

it('should return null for invalid format with only two parts', () => {
expect(parseModelString('claude-sonnet')).toBeNull();
});
Expand Down
Loading