Skip to content
Open
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
77 changes: 77 additions & 0 deletions apps/ccusage/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"pricingSource": {
"type": "string",
"enum": [
"auto",
"litellm",
"modelsdev"
],
"description": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"markdownDescription": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"default": "auto"
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
Expand Down Expand Up @@ -177,6 +188,17 @@
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"pricingSource": {
"type": "string",
"enum": [
"auto",
"litellm",
"modelsdev"
],
"description": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"markdownDescription": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"default": "auto"
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
Expand Down Expand Up @@ -292,6 +314,17 @@
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"pricingSource": {
"type": "string",
"enum": [
"auto",
"litellm",
"modelsdev"
],
"description": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"markdownDescription": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"default": "auto"
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
Expand Down Expand Up @@ -391,6 +424,17 @@
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"pricingSource": {
"type": "string",
"enum": [
"auto",
"litellm",
"modelsdev"
],
"description": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"markdownDescription": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"default": "auto"
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
Expand Down Expand Up @@ -495,6 +539,17 @@
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"pricingSource": {
"type": "string",
"enum": [
"auto",
"litellm",
"modelsdev"
],
"description": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"markdownDescription": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"default": "auto"
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
Expand Down Expand Up @@ -599,6 +654,17 @@
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": false
},
"pricingSource": {
"type": "string",
"enum": [
"auto",
"litellm",
"modelsdev"
],
"description": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"markdownDescription": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"default": "auto"
},
"color": {
"type": "boolean",
"description": "Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.",
Expand Down Expand Up @@ -666,6 +732,17 @@
"markdownDescription": "Use cached pricing data for Claude models instead of fetching from API",
"default": true
},
"pricingSource": {
"type": "string",
"enum": [
"auto",
"litellm",
"modelsdev"
],
"description": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"markdownDescription": "Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)",
"default": "auto"
},
"visualBurnRate": {
"type": "string",
"enum": [
Expand Down
20 changes: 10 additions & 10 deletions apps/ccusage/package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"name": "ccusage",
"type": "module",
"version": "17.1.8",
"description": "Usage analysis tool for Claude Code",
"author": "ryoppippi",
"license": "MIT",
"funding": "https://github.com/ryoppippi/ccusage?sponsor=1",
"homepage": "https://github.com/ryoppippi/ccusage#readme",
"bugs": {
"url": "https://github.com/ryoppippi/ccusage/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ryoppippi/ccusage.git"
},
"bugs": {
"url": "https://github.com/ryoppippi/ccusage/issues"
},
"funding": "https://github.com/ryoppippi/ccusage?sponsor=1",
"license": "MIT",
"author": "ryoppippi",
"type": "module",
"exports": {
".": "./src/index.ts",
"./calculate-cost": "./src/calculate-cost.ts",
Expand All @@ -32,9 +32,6 @@
"config-schema.json",
"dist"
],
"engines": {
"node": ">=20.19.4"
},
"scripts": {
"build": "pnpm run generate:schema && tsdown",
"format": "pnpm run lint --fix",
Expand Down Expand Up @@ -90,6 +87,9 @@
"vitest": "catalog:testing",
"xdg-basedir": "catalog:runtime"
},
"engines": {
"node": ">=20.19.4"
},
"publishConfig": {
"bin": {
"ccusage": "./dist/index.js"
Expand Down
7 changes: 5 additions & 2 deletions apps/ccusage/src/_live-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import type { LoadedUsageEntry, SessionBlock } from './_session-blocks.ts';
import type { CostMode, SortOrder } from './_types.ts';
import type { CostMode, PricingSource, SortOrder } from './_types.ts';
import { readFile, stat } from 'node:fs/promises';
import { Result } from '@praha/byethrow';
import pLimit from 'p-limit';
Expand All @@ -34,6 +34,7 @@ export type LiveMonitorConfig = {
sessionDurationHours: number;
mode: CostMode;
order: SortOrder;
pricingSource: PricingSource;
};

/**
Expand Down Expand Up @@ -72,7 +73,7 @@ async function isRecentFile(filePath: string, cutoffTime: Date): Promise<boolean
* Creates a new live monitoring state
*/
export function createLiveMonitorState(config: LiveMonitorConfig): LiveMonitorState {
const fetcher = config.mode !== 'display' ? new PricingFetcher() : null;
const fetcher = config.mode !== 'display' ? new PricingFetcher(false, config.pricingSource) : null;

return {
fetcher,
Expand Down Expand Up @@ -296,6 +297,7 @@ if (import.meta.vitest != null) {
sessionDurationHours: 5,
mode: 'display',
order: 'desc',
pricingSource: 'auto',
};

state = createLiveMonitorState(config);
Expand Down Expand Up @@ -333,6 +335,7 @@ if (import.meta.vitest != null) {
sessionDurationHours: 5,
mode: 'display' as const,
order: 'desc' as const,
pricingSource: 'auto' as const,
};

using emptyState = createLiveMonitorState(emptyConfig);
Expand Down
3 changes: 2 additions & 1 deletion apps/ccusage/src/_live-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import type { TerminalManager } from '@ccusage/terminal/utils';
import type { SessionBlock } from './_session-blocks.ts';
import type { CostMode, SortOrder } from './_types.ts';
import type { CostMode, PricingSource, SortOrder } from './_types.ts';
import { formatCurrency, formatModelsDisplay, formatNumber } from '@ccusage/terminal/table';
import { centerText, createProgressBar, drawEmoji } from '@ccusage/terminal/utils';
import { delay } from '@std/async';
Expand Down Expand Up @@ -49,6 +49,7 @@ export type LiveMonitoringConfig = {
sessionDurationHours: number;
mode: CostMode;
order: SortOrder;
pricingSource: PricingSource;
};

/**
Expand Down
31 changes: 30 additions & 1 deletion apps/ccusage/src/_pricing-fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { PricingSource } from './_types.ts';
import { LiteLLMPricingFetcher } from '@ccusage/internal/pricing';
import { Result } from '@praha/byethrow';
import { prefetchClaudePricing } from './_macro.ts' with { type: 'macro' };
Expand All @@ -13,13 +14,41 @@ const CLAUDE_PROVIDER_PREFIXES = [

const PREFETCHED_CLAUDE_PRICING = prefetchClaudePricing();

/**
* Determines whether to use models.dev based on pricing source setting
* @param pricingSource - The pricing source mode ('auto', 'litellm', or 'modelsdev')
* @param offline - Whether offline mode is enabled
* @returns true if models.dev should be used, false otherwise
*/
function shouldUseModelsDev(pricingSource: PricingSource, offline: boolean): boolean {
if (offline) {
return false; // Never use models.dev in offline mode
}

switch (pricingSource) {
case 'auto':
return true; // Use both sources (merged)
case 'litellm':
return false; // LiteLLM only
case 'modelsdev':
return true; // models.dev only (will be handled by fetcher options)
default:
return true; // Default to auto
}
}
Comment on lines +17 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

modelsdev mode is indistinguishable from auto in current wiring

shouldUseModelsDev collapses both "auto" and "modelsdev" into useModelsDev = true, and PricingFetcher only passes this boolean into LiteLLMPricingFetcher. Since the underlying fetcher always fetches LiteLLM first and then optionally merges models.dev data, there is no way for pricingSource: "modelsdev" to mean “models.dev only” as advertised; it still hits LiteLLM and prefers LiteLLM data on key collisions.

To align behavior with the CLI modes, consider:

  • Passing the full PricingSource (or an explicit "auto" | "litellm" | "modelsdev" mode) into LiteLLMPricingFetcher instead of a boolean, and
  • Branching in the internal fetch logic so "modelsdev" only ever calls models.dev (with an optional offline fallback) and does not require LiteLLM to be reachable.

This will also let you add tests that assert LiteLLM is not contacted when pricingSource === "modelsdev".

Also applies to: 41-52

🤖 Prompt for AI Agents
In apps/ccusage/src/_pricing-fetcher.ts around lines 17-38 (and likewise 41-52),
the function shouldUseModelsDev currently collapses "auto" and "modelsdev" into
a boolean that causes the fetcher to always contact LiteLLM first; change the
wiring to pass the full PricingSource ("auto" | "litellm" | "modelsdev") into
LiteLLMPricingFetcher instead of a boolean, update LiteLLMPricingFetcher to
branch on that mode so that "modelsdev" triggers only models.dev calls (with an
optional offline fallback) and never contacts LiteLLM, keep "auto" as the merged
behavior and "litellm" as LiteLLM-only, and add tests asserting that when
pricingSource === "modelsdev" LiteLLM is not contacted.


export class PricingFetcher extends LiteLLMPricingFetcher {
constructor(offline = false) {
constructor(offline = false, pricingSource: PricingSource = 'auto') {
// For 'modelsdev' mode, we still need LiteLLM fetcher but only use models.dev data
// This is handled by the useModelsDev flag and the fetcher will merge appropriately
const useModelsDev = shouldUseModelsDev(pricingSource, offline);

super({
offline,
offlineLoader: async () => PREFETCHED_CLAUDE_PRICING,
logger,
providerPrefixes: CLAUDE_PROVIDER_PREFIXES,
useModelsDev,
});
}
}
Expand Down
45 changes: 33 additions & 12 deletions apps/ccusage/src/_shared-args.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { Args } from 'gunshi';
import type { CostMode, SortOrder } from './_types.ts';
import type { CostMode, PricingSource, SortOrder } from './_types.ts';
import * as v from 'valibot';
import { DEFAULT_LOCALE } from './_consts.ts';
import { CostModes, filterDateSchema, SortOrders } from './_types.ts';
import {
CostModes,
filterDateSchema,
PricingSources,
SortOrders,
} from './_types.ts';

/**
* Parses and validates a date argument in YYYYMMDD format
Expand Down Expand Up @@ -39,7 +44,7 @@ export const sharedArgs = {
type: 'enum',
short: 'm',
description:
'Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)',
'Cost calculation mode: auto (use costUSD if exists, otherwise calculate), calculate (always calculate), display (always use costUSD)',
default: 'auto' as const satisfies CostMode,
choices: CostModes,
},
Expand All @@ -52,7 +57,7 @@ export const sharedArgs = {
debugSamples: {
type: 'number',
description:
'Number of sample discrepancies to show in debug output (default: 5)',
'Number of sample discrepancies to show in debug output (default: 5)',
default: 5,
},
order: {
Expand All @@ -72,21 +77,35 @@ export const sharedArgs = {
type: 'boolean',
negatable: true,
short: 'O',
description: 'Use cached pricing data for Claude models instead of fetching from API',
description:
'Use cached pricing data for Claude models instead of fetching from API',
default: false,
},
color: { // --color and FORCE_COLOR=1 is handled by picocolors
pricingSource: {
type: 'enum',
short: 'p',
description:
'Pricing data source: auto (merge LiteLLM + models.dev), litellm (LiteLLM only), modelsdev (models.dev only)',
default: 'auto' as const satisfies PricingSource,
choices: PricingSources,
},
color: {
// --color and FORCE_COLOR=1 is handled by picocolors
type: 'boolean',
description: 'Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.',
description:
'Enable colored output (default: auto). FORCE_COLOR=1 has the same effect.',
},
noColor: { // --no-color and NO_COLOR=1 is handled by picocolors
noColor: {
// --no-color and NO_COLOR=1 is handled by picocolors
type: 'boolean',
description: 'Disable colored output (default: auto). NO_COLOR=1 has the same effect.',
description:
'Disable colored output (default: auto). NO_COLOR=1 has the same effect.',
},
timezone: {
type: 'string',
short: 'z',
description: 'Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone',
description:
'Timezone for date grouping (e.g., UTC, America/New_York, Asia/Tokyo). Default: system timezone',
},
locale: {
type: 'string',
Expand All @@ -97,15 +116,17 @@ export const sharedArgs = {
jq: {
type: 'string',
short: 'q',
description: 'Process JSON output with jq command (requires jq binary, implies --json)',
description:
'Process JSON output with jq command (requires jq binary, implies --json)',
},
config: {
type: 'string',
description: 'Path to configuration file (default: auto-discovery)',
},
compact: {
type: 'boolean',
description: 'Force compact mode for narrow displays (better for screenshots)',
description:
'Force compact mode for narrow displays (better for screenshots)',
default: false,
},
} as const satisfies Args;
Expand Down
13 changes: 13 additions & 0 deletions apps/ccusage/src/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,19 @@ export const CostModes = ['auto', 'calculate', 'display'] as const;
*/
export type CostMode = TupleToUnion<typeof CostModes>;

/**
* Available pricing data sources
* - auto: Use both LiteLLM and models.dev (merged, LiteLLM takes precedence)
* - litellm: Use only LiteLLM pricing data
* - modelsdev: Use only models.dev pricing data
*/
export const PricingSources = ['auto', 'litellm', 'modelsdev'] as const;

/**
* Union type for pricing sources
*/
export type PricingSource = TupleToUnion<typeof PricingSources>;

/**
* Available sort orders for data presentation
*/
Expand Down
Loading