feat: add offline mode support for cost calculations#79
Conversation
commit: |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Summary of Changes
Hello @ryoppippi, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request introduces a new offline mode for cost calculations within the application. By leveraging build-time data pre-fetching, it allows users to perform cost analyses using cached pricing information, eliminating the need for real-time API calls. This enhancement improves performance and ensures functionality even without an active internet connection, providing a more robust and efficient user experience.
Highlights
- Offline Mode CLI Flag: A new
--offline(or-O) flag has been added to thedaily,monthly, andsessioncommands, enabling users to explicitly request cost calculations using cached pricing data. - Build-Time Pricing Data Pre-fetching: The application now utilizes
unplugin-macrosto fetch and embed the latest Claude model pricing data from LiteLLM directly into the build artifact. This ensures that pricing information is available locally for offline use. - Enhanced PricingFetcher: The
PricingFetcherclass has been updated to accept anofflineparameter, allowing it to intelligently switch between fetching live pricing data and utilizing the pre-fetched, embedded data. - Robust Offline Functionality: Comprehensive error handling is implemented for offline scenarios, providing graceful fallbacks and clear warning messages if pre-fetched data is unavailable, ensuring the application continues to function.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments to provide feedback.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Code Review
This pull request introduces an offline mode for cost calculations by pre-fetching pricing data at build time using a macro. The changes are well-implemented, with good error handling and test coverage for the new functionality. One minor suggestion is provided to enhance the clarity of build-time error messages.
There was a problem hiding this comment.
Actionable comments posted: 6
🔭 Outside diff range comments (2)
src/pricing-fetcher.ts (1)
98-106: Missing new Claude-4 naming variant leads to false negatives
getModelPricing()’s heuristic list does not include the newclaude-4-*naming that the tests (and likely users) now pass in:`claude-4-${modelName}`,Without it, look-ups such as
"claude-sonnet-4-20250514"→"claude-4-sonnet-20250514"will fail.Either extend the variations array or normalise both strings before comparison. Quick patch:
const variations = [ modelName, `anthropic/${modelName}`, `claude-3-5-${modelName}`, `claude-3-${modelName}`, + `claude-4-${modelName}`, `claude-${modelName}`, ];Make sure to add unit tests covering both old and new orders.
src/pricing-fetcher.test.ts (1)
81-92: Tests use the old model naming and will now failThe implementation and other tests were migrated to
"claude-4-sonnet-20250514"but these two still reference"claude-sonnet-4-20250514":-const pricing = await fetcher.getModelPricing('claude-sonnet-4-20250514'); +const pricing = await fetcher.getModelPricing('claude-4-sonnet-20250514');and
-const pricing = await fetcher.getModelPricing('claude-sonnet-4'); +const pricing = await fetcher.getModelPricing('claude-4-sonnet');Update them (and the test titles) or add the missing variant handling in
PricingFetcher.
♻️ Duplicate comments (2)
src/commands/daily.ts (1)
31-32: Mirror the type-safety fix here as wellSame
ts/no-unsafe-member-accessfinding forctx.values.offline.
Apply whichever solution you pick forsession.tsto stay consistent.src/data-loader.ts (1)
607-608: Duplicate compile issue – see previous commentThe same nullable-
usingpattern appears here; refactor as described above.
🧹 Nitpick comments (6)
src/consts.internal.ts (1)
1-2: Minor: add a literal-type assertion for stronger immutabilityExporting the URL verbatim works, but appending
as constprevents accidental re-assignment or widening by consumers.-export const LITELLM_PRICING_URL - = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json'; +export const LITELLM_PRICING_URL = + 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json' as const;src/commands/session.ts (1)
31-32: Type-safety warning surfaced by ESLint
ctxis inferred asany, givingts/no-unsafe-*errors every time we touchctx.values. The newofflineaccess just adds another occurrence.Consider one of:
const sessionCommand = define<{ values: typeof sharedArgs extends { args: infer A } ? v.Output<A> : never }>( {or simply suppress for now:
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access offline: (ctx.values as Record<string, unknown>).offline as boolean,This keeps the rule from becoming noisy across new lines.
src/commands/monthly.ts (1)
31-31:ctx.valuesisany– propagate proper typing to avoidno-unsafe-*ESLint noise
ctxcomes fromgunshi.define, so you can extend the generic parameter to describevalues(e.g.{ offline?: boolean }).
Doing so eliminates the unsafe‐access lint errors and guards against typos at call-sites.tsdown.config.ts (2)
2-2: Import is fine but unused in file scope
Macrosis referenced only inside thepluginsarray – keep the import but switch toimport Macros from …withassert { type: 'macro' }is unnecessary; however ESLint may flag the current unused-identifier if you later refactor. Consider moving to inlineconst { default: Macros } = await import(...)or add an eslint ignore.
24-28: Plugin array mutates compile pipeline – add fallback handling for build failuresIf the macro fetch fails (e.g. due to network outage during CI) the whole build will abort. Provide
onError/try … catchinside the plugin options to fall back to empty pricing so that offline builds remain deterministic.src/data-loader.ts (1)
463-468:offlineoption added – good, but mark itfalseby defaultDeclare
offline?: booleaninLoadOptionsas you did, and document thatfalseis the default so callers know network fetch remains enabled unless explicitly disabled.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📥 Commits
Reviewing files that changed from the base of the PR and between 932b9ec and ba396ae5fa1195215b437b029ebf97116f1570c6.
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
package.json(1 hunks)src/commands/daily.ts(1 hunks)src/commands/monthly.ts(1 hunks)src/commands/session.ts(1 hunks)src/consts.internal.ts(1 hunks)src/data-loader.test.ts(16 hunks)src/data-loader.ts(3 hunks)src/macro.internal.ts(1 hunks)src/pricing-fetcher.test.ts(6 hunks)src/pricing-fetcher.ts(3 hunks)src/shared-args.internal.ts(1 hunks)tsdown.config.ts(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/macro.internal.ts (2)
src/consts.internal.ts (1)
LITELLM_PRICING_URL(1-2)src/pricing-fetcher.ts (2)
ModelPricing(13-13)ModelPricingSchema(6-11)
src/data-loader.ts (1)
src/pricing-fetcher.ts (1)
PricingFetcher(15-183)
src/pricing-fetcher.ts (2)
src/macro.internal.ts (1)
prefetchClaudeData(32-32)src/logger.ts (1)
logger(5-5)
🪛 ESLint
src/commands/monthly.ts
[error] 31-31: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 31-31: Unsafe member access .values on an any value.
(ts/no-unsafe-member-access)
src/commands/session.ts
[error] 31-31: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 31-31: Unsafe member access .values on an any value.
(ts/no-unsafe-member-access)
src/commands/daily.ts
[error] 31-31: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 31-31: Unsafe member access .values on an any value.
(ts/no-unsafe-member-access)
src/macro.internal.ts
[error] 12-12: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 12-12: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 13-13: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 13-13: Unsafe member access .ok on an error typed value.
(ts/no-unsafe-member-access)
[error] 14-14: Unsafe member access .statusText on an error typed value.
(ts/no-unsafe-member-access)
[error] 18-18: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 18-18: Unsafe member access .json on an error typed value.
(ts/no-unsafe-member-access)
[error] 25-25: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 25-25: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 25-25: Unsafe member access .safeParse on an error typed value.
(ts/no-unsafe-member-access)
[error] 26-26: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 26-26: Unsafe member access .success on an error typed value.
(ts/no-unsafe-member-access)
[error] 27-27: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 27-27: Unsafe member access .output on an error typed value.
(ts/no-unsafe-member-access)
tsdown.config.ts
[error] 25-25: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
src/data-loader.test.ts
[error] 1906-1906: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1907-1907: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe member access .toBe on an error typed value.
(ts/no-unsafe-member-access)
src/pricing-fetcher.test.ts
[error] 256-256: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 257-257: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 264-264: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 264-264: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 264-264: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 270-270: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 270-270: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 270-270: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 274-274: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 274-274: Unsafe member access .warn on an error typed value.
(ts/no-unsafe-member-access)
[error] 278-278: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 290-290: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 290-290: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 290-290: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 294-294: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 294-294: Unsafe member access .warn on an error typed value.
(ts/no-unsafe-member-access)
src/pricing-fetcher.ts
[error] 6-6: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 6-6: Unsafe member access .object on an error typed value.
(ts/no-unsafe-member-access)
[error] 46-46: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 46-46: Unsafe member access .warn on an error typed value.
(ts/no-unsafe-member-access)
🔇 Additional comments (4)
package.json (1)
72-73: Confirm build‐time integration of the new macro plugin
unplugin-macrosis a pure build-time dependency, so placing it underdevDependenciesis correct.
Please double-check that:
tsdown.config.ts(or the bundler config in use) actually registers the plugin – otherwise the macro import will be left untouched at runtime and offline mode will break.- CI caches have been invalidated so the new plugin is installed during the next pipeline run.
No code change needed if both are already covered.
src/shared-args.internal.ts (1)
66-72: Flag name & shortcut look good – verify collisionThe new
offlinearg is well-defined (boolean, negatable, defaultfalse).
Quick sanity checks:
-Ois not already consumed by another command or global flag.- Help output shows both
--offlineand--no-offline.If those are clear, 👍.
src/pricing-fetcher.ts (1)
4-4: Import with `with { type: 'macro' } may break at test/runtime
with { type: 'macro' }relies on the bundler / macro plugin executing before Bun runs the test suite.
When the macro transform is not wired-up (e.g. rawbun testwithout Vite/Rollup), Bun will attempt to evaluate the file directly and fail to recognise this syntax, causing a SyntaxError: import attributes not yet supported.Please verify that:
unplugin-macrosis actually executed in the Bun test runner pipeline.- The compiled artefact that reaches the test/production runtime no longer contains the
withimport attribute.If this is not guaranteed, fall back to a plain import and gate the macro under a build-time
process.env.BUILD_TIMEcheck, or ship a pre-generated JSON artefact instead.src/data-loader.test.ts (1)
1195-1195: Consistent model naming convention updated
All test cases now reference the new prefix'claude-4-sonnet-20250514', aligning with your macro-generated data. Please verify that the PricingFetcher and any related fixtures/mocks also expect and use this updated naming format.Also applies to: 1264-1264, 1335-1335, 1412-1412, 1440-1440, 1470-1470, 1508-1508, 1575-1575, 1606-1606, 1636-1636
There was a problem hiding this comment.
using on a nullable value triggers TS 5.4 compile-time error
using fetcher = mode === 'display' ? null : new PricingFetcher(options?.offline);
null is not Disposable. TypeScript’s using syntax forbids nullable initialisers and this will not compile.
Quick fix:
-let fetcher: PricingFetcher | undefined;
-if (mode !== 'display') {
- fetcher = new PricingFetcher(options?.offline);
-}
-try {
- // …
-} finally {
- fetcher?.[Symbol.dispose]();
-}Apply the same pattern in loadSessionData.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/data-loader.ts around lines 491 to 492, the use of 'using' with a
nullable value (null) causes a TypeScript 5.4 compile error because 'using'
requires a non-null Disposable. To fix this, avoid assigning null to the 'using'
variable; instead, conditionally create the PricingFetcher only when mode is not
'display' and ensure the variable is always a Disposable. Apply the same pattern
in the loadSessionData function to prevent similar errors.
There was a problem hiding this comment.
🛠️ Refactor suggestion
Un-guarded network fetch can break the build
Any transient 503 will throw and fail the macro. Wrap the fetch in a retry or at least swallow the error and export an empty object so that offline builds keep working.
🧰 Tools
🪛 ESLint
[error] 12-12: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 12-12: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 13-13: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 13-13: Unsafe member access .ok on an error typed value.
(ts/no-unsafe-member-access)
[error] 14-14: Unsafe member access .statusText on an error typed value.
(ts/no-unsafe-member-access)
🤖 Prompt for AI Agents
In src/macro.internal.ts around lines 12 to 15, the fetch call to
LITELLM_PRICING_URL is unguarded and will throw an error on transient failures
like HTTP 503, breaking the build. To fix this, wrap the fetch call in a retry
mechanism or catch any errors and return an empty object instead, ensuring that
offline or transient failure scenarios do not cause the macro to fail the build.
There was a problem hiding this comment.
Offline branch skips schema-validation and can silently return an empty map
prefetchClaudeData is trusted wholesale:
const pricing = new Map(Object.entries(prefetchClaudeData));Two issues:
- No validation – any shape mismatch bypasses
ModelPricingSchema, whereas the online path is validated. - If the macro returns
{}(e.g. network failure at build-time) you still resolve successfully and downstream tests crash because the map is empty rather than throwing.
Proposed fix:
-const pricing = new Map(Object.entries(prefetchClaudeData));
+const validated = new Map<string, ModelPricing>();
+for (const [model, raw] of Object.entries(prefetchClaudeData ?? {})) {
+ const parsed = v.safeParse(ModelPricingSchema, raw);
+ if (parsed.success) validated.set(model, parsed.output);
+}
+
+if (validated.size === 0) {
+ throw new Error('No pre-fetched pricing data available');
+}
+
+this.cachedPricing = validated;
+return validated;This keeps behaviour symmetric with the online path and surfaces a clear error when the macro did not provide data.
🧰 Tools
🪛 ESLint
[error] 46-46: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 46-46: Unsafe member access .warn on an error typed value.
(ts/no-unsafe-member-access)
🤖 Prompt for AI Agents
In src/pricing-fetcher.ts around lines 36 to 50, the offline mode uses
prefetchClaudeData without validating it against ModelPricingSchema, unlike the
online path, which causes silent failures if the data shape is incorrect or
empty. To fix this, validate prefetchClaudeData using ModelPricingSchema before
creating the Map, and if validation fails or the data is empty, throw an error
or handle it explicitly to prevent returning an empty map silently.
There was a problem hiding this comment.
🛠️ Refactor suggestion
Skipping tests via try…catch masks real failures
Catching any error inside the test still runs the expect assertions after the failure point – ESLint rightfully flags “unsafe call of an error typed value”.
Prefer Bun’s explicit skip:
-it('should use pre-fetched data in offline mode when available', async () => {
- using fetcher = new PricingFetcher(true); // offline mode
- try {
- const pricing = await fetcher.fetchModelPricing();
- …
- } catch {
- console.warn('Offline mode test skipped: macro data not available');
- }
-});
+it.skip('should use pre-fetched data in offline mode when available (macro missing)', async () => {
+ using fetcher = new PricingFetcher(true);
+ const pricing = await fetcher.fetchModelPricing();
+ …
+});You can programmatically decide to skip:
const pricing = await fetcher.fetchModelPricing();
if (pricing.size === 0) return test.skip();This avoids swallowing legitimate errors and removes the ESLint warnings.
🧰 Tools
🪛 ESLint
[error] 256-256: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 257-257: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 264-264: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 264-264: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 264-264: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 270-270: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 270-270: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 270-270: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 274-274: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 274-274: Unsafe member access .warn on an error typed value.
(ts/no-unsafe-member-access)
[error] 278-278: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 290-290: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 290-290: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 290-290: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 294-294: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 294-294: Unsafe member access .warn on an error typed value.
(ts/no-unsafe-member-access)
🤖 Prompt for AI Agents
In src/pricing-fetcher.test.ts around lines 256 to 296, the tests use try-catch
blocks to skip tests when data is unavailable, which masks real failures and
causes ESLint warnings. Remove the try-catch blocks and instead check the
condition after fetching data; if the data is missing or empty, call test.skip()
to explicitly skip the test. This approach prevents swallowing errors and
resolves ESLint unsafe call warnings.
There was a problem hiding this comment.
🛠️ Refactor suggestion
Improve offline-mode test specificity
The added offline-mode suite currently only checks that loadDailyUsageData({ offline: true, mode: 'calculate' }) returns an array without throwing. To fully validate offline behavior, mock or spy on your network/fetch logic (e.g., PricingFetcher.fetchPricing) and assert that no HTTP calls occur and cached data is used when offline: true.
🧰 Tools
🪛 ESLint
[error] 1906-1906: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1907-1907: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe member access .toBe on an error typed value.
(ts/no-unsafe-member-access)
🤖 Prompt for AI Agents
In src/data-loader.test.ts around lines 1906 to 1917, the offline mode test only
verifies that loadDailyUsageData returns an array without throwing when called
with offline: true. To improve this, mock or spy on the network or fetch
function used internally (such as PricingFetcher.fetchPricing) and add
assertions to confirm that no HTTP calls are made during the test, ensuring
cached data is used when offline is true.
- Add macro system for build-time pricing data fetching from LiteLLM - Implement automatic caching of all claude- model pricing data - Add --offline CLI flag to shared arguments for all commands - Create constants file for internal configuration - Enable offline usage without network dependency on LiteLLM API - Support both online and offline cost calculation modes This provides the foundation for offline mode functionality where pricing data is pre-fetched at build time and embedded in the binary.
- Add offline flag support to LoadOptions interface in data-loader - Implement offline mode in PricingFetcher using pre-fetched macro data - Add caching logic for offline pricing data to avoid repeated object creation - Return cached pricing data when offline flag is enabled - Provide graceful fallback when macro data is unavailable in offline mode - Add proper error handling for offline mode edge cases The pricing fetcher now supports both online (LiteLLM API) and offline (build-time cached) modes, enabling usage without network connectivity.
- Pass offline flag from CLI arguments to data loading functions in all commands - Enable macro processing for pricing-fetcher in tsdown build configuration - Connect --offline flag support to daily, monthly, and session commands - Allow macro expansion during build process for offline pricing data This completes the integration of offline mode throughout the CLI, enabling users to run all commands with --offline flag support.
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (2)
src/commands/daily.ts (1)
25-32:ctx.valuesis still typed asany; ESLint errors will persistThe new
offlineprop itself is fine, but the repetitivets/no-unsafe-*errors stem fromctxbeing implicitlyany.
Define a proper generic when callingdefine()(or castctx), so allctx.values.*properties are strongly-typed and you don’t have to suppress/disable the rule in every command.-export const dailyCommand = define({ +export const dailyCommand = define<{ // 🎯 strong typing + offline?: boolean; + // …other flags +}>({(Do the same for the other commands.)
src/pricing-fetcher.test.ts (1)
80-84: Model name mismatch – old string remains.Most tests were migrated to the new prefix (
claude-4-…) but this assertion still requests'claude-sonnet-4-20250514'.
Update for consistency to avoid false negatives:-const pricing = await fetcher.getModelPricing('claude-sonnet-4-20250514'); +const pricing = await fetcher.getModelPricing('claude-4-sonnet-20250514');
♻️ Duplicate comments (8)
src/commands/monthly.ts (1)
25-32: Same typing issue as indailyCommand– address via a generic ondefine<>()or a dedicated interface forctx.values, otherwise thets/no-unsafe-*warnings stay.src/commands/session.ts (1)
25-32: Same typing issue as indailyCommand– introduce a typed context to removeanyusage.src/macro.internal.ts (3)
18-21: Error message omits the URLInclude
LITELLM_PRICING_URLso users immediately see which endpoint failed.-throw new Error(`Failed to fetch pricing data: ${response.statusText}`); +throw new Error( + `Failed to fetch pricing data from ${LITELLM_PRICING_URL}: ${response.statusText}`, +);
6-8: Circular dependency withpricing-fetcher.tsis still unresolvedImporting
ModelPricingSchemafrompricing-fetcher.tswhile that file importsprefetchClaudePricing→ runtimeundefinedrisk.-import { type ModelPricing, ModelPricingSchema } from './pricing-fetcher.ts'; +import { type ModelPricing, ModelPricingSchema } from './model-pricing.schema.ts'; // move schemaExtract the schema to a standalone module and have both files import from there.
18-23: Unguarded fetch will fail hard during transient outagesA single 503 will abort the build. Wrap the request in a minimal retry or, at the very least, swallow the error and return
{}to keep offline builds working.-const response = await fetch(LITELLM_PRICING_URL); -if (!response.ok) { - throw new Error(`Failed to fetch pricing data: ${response.statusText}`); -} +let response: Response | undefined; +for (let attempt = 0; attempt < 3; attempt++) { + try { + response = await fetch(LITELLM_PRICING_URL); + if (response.ok) break; + } catch { /* ignore */ } + await new Promise(r => setTimeout(r, 500 * (attempt + 1))); +} +if (!response?.ok) return {}; // graceful fallbacksrc/data-loader.test.ts (1)
1906-1917: Offline-mode test still only asserts “no throw”.Previous review already asked to spy/mock the fetch path (e.g.
PricingFetcher.fetchModelPricing) and assert no network activity. That feedback still applies.src/pricing-fetcher.ts (1)
36-41: Offline branch still returns an un-validated, possibly empty map.This was pointed out in a previous review and hasn’t been addressed.
prefetchClaudePricing()may yield{}(e.g. build-time fetch failed) yet you cache & return it silently, breaking cost calculations downstream.Recommend validating and throwing when zero items are loaded:
-const pricing = new Map(Object.entries(await prefetchClaudePricing())); -this.cachedPricing = pricing; -return pricing; +const raw = await prefetchClaudePricing(); +const pricing = new Map(Object.entries(raw)); +if (pricing.size === 0) { + throw new Error('Offline mode: no pre-fetched pricing data available'); +} +this.cachedPricing = pricing; +return pricing;src/pricing-fetcher.test.ts (1)
232-261: Offline-mode tests will fail when macro data is absent.
prefetchClaudePricingcan legitimately return an empty record (network down during build).
If that happens, these tests should be skipped, not fail.-const pricing = await fetcher.fetchModelPricing(); -// Should have Claude models from pre-fetched data -expect(pricing.size).toBeGreaterThan(0); +const pricing = await fetcher.fetchModelPricing(); +if (pricing.size === 0) { + // No macro data – skip to avoid false red. + return test.skip('pre-fetched pricing unavailable'); +} +expect(pricing.size).toBeGreaterThan(0);Apply the same guard to the cost-calculation case.
🧹 Nitpick comments (1)
tsdown.config.ts (1)
24-28: Plugin include glob is too narrow – macro may be missed
prefetchClaudePricingis defined insrc/macro.internal.tsbut could later be invoked from files outside the current include list (e.g. a new helper). Consider widening the pattern or using the defaultincludeso the macro keeps functioning if its call-sites expand.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📥 Commits
Reviewing files that changed from the base of the PR and between ba396ae5fa1195215b437b029ebf97116f1570c6 and 8e53c52a76ef17802edd225976c0d27a4a68b7e1.
📒 Files selected for processing (11)
src/commands/daily.ts(1 hunks)src/commands/monthly.ts(1 hunks)src/commands/session.ts(1 hunks)src/consts.internal.ts(1 hunks)src/data-loader.test.ts(16 hunks)src/data-loader.ts(3 hunks)src/macro.internal.ts(1 hunks)src/pricing-fetcher.test.ts(3 hunks)src/pricing-fetcher.ts(3 hunks)src/shared-args.internal.ts(1 hunks)tsdown.config.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/data-loader.ts
- src/consts.internal.ts
- src/shared-args.internal.ts
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/data-loader.test.ts (1)
src/data-loader.ts (1)
loadDailyUsageData(470-584)
src/macro.internal.ts (2)
src/pricing-fetcher.ts (2)
ModelPricing(13-13)ModelPricingSchema(6-11)src/consts.internal.ts (1)
LITELLM_PRICING_URL(1-2)
src/pricing-fetcher.test.ts (1)
src/pricing-fetcher.ts (1)
PricingFetcher(15-174)
src/pricing-fetcher.ts (1)
src/macro.internal.ts (1)
prefetchClaudePricing(17-38)
🪛 ESLint
src/commands/daily.ts
[error] 31-31: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 31-31: Unsafe member access .values on an any value.
(ts/no-unsafe-member-access)
src/commands/monthly.ts
[error] 31-31: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 31-31: Unsafe member access .values on an any value.
(ts/no-unsafe-member-access)
src/commands/session.ts
[error] 31-31: Unsafe assignment of an any value.
(ts/no-unsafe-assignment)
[error] 31-31: Unsafe member access .values on an any value.
(ts/no-unsafe-member-access)
src/data-loader.test.ts
[error] 1906-1906: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1907-1907: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 1916-1916: Unsafe member access .toBe on an error typed value.
(ts/no-unsafe-member-access)
src/macro.internal.ts
[error] 18-18: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 18-18: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 19-19: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 19-19: Unsafe member access .ok on an error typed value.
(ts/no-unsafe-member-access)
[error] 20-20: Unsafe member access .statusText on an error typed value.
(ts/no-unsafe-member-access)
[error] 23-23: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 23-23: Unsafe member access .json on an error typed value.
(ts/no-unsafe-member-access)
[error] 30-30: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 30-30: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 30-30: Unsafe member access .safeParse on an error typed value.
(ts/no-unsafe-member-access)
[error] 31-31: Unexpected any value in conditional. An explicit comparison or type conversion is required.
(ts/strict-boolean-expressions)
[error] 31-31: Unsafe member access .success on an error typed value.
(ts/no-unsafe-member-access)
[error] 32-32: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 32-32: Unsafe member access .output on an error typed value.
(ts/no-unsafe-member-access)
src/pricing-fetcher.test.ts
[error] 108-108: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 111-111: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 111-111: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 111-111: Unsafe member access .not on an error typed value.
(ts/no-unsafe-member-access)
[error] 112-112: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 112-112: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 112-112: Unsafe member access .input_cost_per_token on an error typed value.
(ts/no-unsafe-member-access)
[error] 112-112: Unsafe member access .not on an error typed value.
(ts/no-unsafe-member-access)
[error] 113-113: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 113-113: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 113-113: Unsafe member access .output_cost_per_token on an error typed value.
(ts/no-unsafe-member-access)
[error] 113-113: Unsafe member access .not on an error typed value.
(ts/no-unsafe-member-access)
[error] 120-120: This assertion is unnecessary since the receiver accepts the original type of the expression.
(ts/no-unnecessary-type-assertion)
[error] 123-123: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 123-123: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 123-123: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 126-126: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 129-129: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 138-138: This assertion is unnecessary since the receiver accepts the original type of the expression.
(ts/no-unnecessary-type-assertion)
[error] 142-142: Unsafe member access .input_cost_per_token on an error typed value.
(ts/no-unsafe-member-access)
[error] 143-143: Unsafe member access .output_cost_per_token on an error typed value.
(ts/no-unsafe-member-access)
[error] 144-144: Unsafe member access .cache_creation_input_token_cost on an error typed value.
(ts/no-unsafe-member-access)
[error] 145-145: Unsafe member access .cache_read_input_token_cost on an error typed value.
(ts/no-unsafe-member-access)
[error] 147-147: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 147-147: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 147-147: Unsafe member access .toBeCloseTo on an error typed value.
(ts/no-unsafe-member-access)
[error] 148-148: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 148-148: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 148-148: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 151-151: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 154-154: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 157-157: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 157-157: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 157-157: Unsafe member access .not on an error typed value.
(ts/no-unsafe-member-access)
[error] 158-158: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 158-158: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 158-158: Unsafe member access .input_cost_per_token on an error typed value.
(ts/no-unsafe-member-access)
[error] 158-158: Unsafe member access .not on an error typed value.
(ts/no-unsafe-member-access)
[error] 159-159: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 159-159: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 159-159: Unsafe member access .output_cost_per_token on an error typed value.
(ts/no-unsafe-member-access)
[error] 159-159: Unsafe member access .not on an error typed value.
(ts/no-unsafe-member-access)
[error] 166-166: This assertion is unnecessary since the receiver accepts the original type of the expression.
(ts/no-unnecessary-type-assertion)
[error] 169-169: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 169-169: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 169-169: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 172-172: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 175-175: Unsafe assignment of an error typed value.
(ts/no-unsafe-assignment)
[error] 184-184: This assertion is unnecessary since the receiver accepts the original type of the expression.
(ts/no-unnecessary-type-assertion)
[error] 188-188: Unsafe member access .input_cost_per_token on an error typed value.
(ts/no-unsafe-member-access)
[error] 189-189: Unsafe member access .output_cost_per_token on an error typed value.
(ts/no-unsafe-member-access)
[error] 190-190: Unsafe member access .cache_creation_input_token_cost on an error typed value.
(ts/no-unsafe-member-access)
[error] 191-191: Unsafe member access .cache_read_input_token_cost on an error typed value.
(ts/no-unsafe-member-access)
[error] 232-232: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 233-233: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 239-239: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 239-239: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 239-239: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 245-245: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 245-245: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 245-245: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
[error] 248-248: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 259-259: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 259-259: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 259-259: Unsafe member access .toBeGreaterThan on an error typed value.
(ts/no-unsafe-member-access)
src/pricing-fetcher.ts
[error] 6-6: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
[error] 6-6: Unsafe member access .object on an error typed value.
(ts/no-unsafe-member-access)
tsdown.config.ts
[error] 25-25: Unsafe call of a(n) error type typed value.
(ts/no-unsafe-call)
There was a problem hiding this comment.
Pull Request Overview
This PR enables offline mode for cost calculations by embedding model pricing data at build time and adding a CLI flag to toggle cached data usage.
- Adds a macro plugin to prefetch and embed pricing data during build
- Introduces
ModelPricingSchemaand centralizes the pricing URL constant - Implements
--offlineflag throughout CLI, updatesPricingFetcherto support offline mode, and adds corresponding tests
Reviewed Changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tsdown.config.ts | Configures the unplugin-macros rollup plugin for build-time pricing fetch |
| src/types.internal.ts | Defines ModelPricingSchema and ModelPricing type |
| src/shared-args.internal.ts | Adds offline boolean flag to shared CLI arguments |
| src/pricing-fetcher.ts | Updates PricingFetcher to accept an offline flag and use prefetch macro |
| src/pricing-fetcher.test.ts | Adapts tests for new model naming and adds offline-mode test cases |
| src/macro.internal.ts | Implements prefetchClaudePricing() macro to fetch and validate pricing |
| src/data-loader.ts | Propagates offline option into data-loading functions |
| src/data-loader.test.ts | Updates model naming in tests and adds offline-mode load test |
| src/consts.internal.ts | Extracts pricing URL constant |
| src/commands/{daily,monthly,session}.ts | Passes offline flag into command handlers |
| package.json | Adds unplugin-macros dependency |
Comments suppressed due to low confidence (2)
src/pricing-fetcher.test.ts:235
- The test calls
fetchModelPricing(), but thePricingFetcherclass providesgetModelPricing()(and nofetchModelPricingmethod). Update the test to usegetModelPricing(modelName)or add an alias that returns all pricing data.
const pricing = await fetcher.fetchModelPricing();
src/pricing-fetcher.test.ts:1
- The imported
ModelPricingtype is not used anywhere in this test file; consider removing it to clean up unused imports.
import type { ModelPricing } from './types.internal.ts';
…uality - Add offline mode integration tests for data-loader functions - Add extensive offline mode tests for PricingFetcher class - Test consistency between online and offline cost calculations - Update model names to use correct Claude 4 format (claude-sonnet-4-20250514) - Remove unnecessary conditional branches and try-catch blocks from tests - Simplify test code while maintaining coverage and error handling - Add validation for pre-fetched pricing data in offline mode - Ensure offline functionality works end-to-end across the application Tests now provide comprehensive coverage of offline mode functionality while being more maintainable and predictable in their execution.
…ro modules - Move ModelPricing type and ModelPricingSchema from pricing-fetcher.ts to types.internal.ts - Update imports in pricing-fetcher.ts, macro.internal.ts, and pricing-fetcher.test.ts - Fix ESLint import ordering issues in test file - Eliminate circular dependency where pricing-fetcher imported from macro.internal via macro import, while macro.internal imported types from pricing-fetcher This change improves module architecture by centralizing shared types in the types module, preventing the circular import chain that could cause build or runtime issues.
feat: add offline mode support for cost calculations

Summary
This PR adds comprehensive offline mode support to enable cost calculations without network connectivity to the LiteLLM API. The implementation uses a build-time macro system to pre-fetch and embed pricing data directly into the binary.
Key Features
--offlineflag support to all commands (daily, monthly, session)Technical Implementation
Usage
Test Coverage
This change maintains full backward compatibility while adding the flexibility to work without network connectivity.
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores