Skip to content

feat: add --fetch CLI argument for custom model pricing data sources - #51

Closed
iamwrm wants to merge 1 commit into
ccusage:mainfrom
iamwrm:feature/fetch-cli-argument
Closed

feat: add --fetch CLI argument for custom model pricing data sources#51
iamwrm wants to merge 1 commit into
ccusage:mainfrom
iamwrm:feature/fetch-cli-argument

Conversation

@iamwrm

@iamwrm iamwrm commented Jun 10, 2025

Copy link
Copy Markdown

Summary

Changes

  • Added --fetch argument to shared CLI arguments with support for both HTTPS URLs and local filesystem paths
  • Updated PricingFetcher class to accept custom data sources in constructor
  • Modified all commands (daily, monthly, session) to pass the fetch parameter
  • Updated debug functionality to use custom pricing sources
  • Maintained full backward compatibility with default LiteLLM pricing

Test plan

  • Build succeeds without errors
  • All existing tests pass (106/106)
  • Linting passes
  • Manual testing with both local file and HTTPS URL works correctly
  • Default behavior unchanged when --fetch is not specified
  • Help text displays the new option correctly

Examples

Using a local file:

ccusage daily --fetch /path/to/pricing.json

Using a custom HTTPS URL:

ccusage daily --fetch https://example.com/custom-pricing.json

Resolves #12

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new command-line option to specify a custom source (URL or local file) for model pricing data in all commands.
    • Enhanced flexibility for cost calculations by allowing the use of external or local pricing databases.
  • Documentation

    • Updated documentation to explain the new option, its usage, and its effect on different calculation modes, including usage examples.

Copilot AI review requested due to automatic review settings June 10, 2025 14:48

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Summary of Changes

Hello @iamwrm, 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 implements the requested feature to allow users to override the default model pricing data source. By introducing a new --fetch CLI argument, users can now point the application to a custom URL or a local file containing pricing information, which is particularly useful for users behind corporate proxies or those who wish to use alternative pricing data. The core logic for fetching/loading pricing data has been updated to handle both URL and file inputs, and this new capability is integrated into the main commands and debug features.

Highlights

  • New CLI Argument: I've added a new --fetch (or -f) command-line argument to allow users to specify a custom source for model pricing data. This source can be either an HTTPS URL or a local file path.
  • Custom Pricing Source Support: The PricingFetcher class has been updated to accept an optional source URL or file path in its constructor. If no source is provided, it defaults to the existing LiteLLM pricing URL, maintaining backward compatibility.
  • Integration Across Commands: The new --fetch argument is now passed down through the daily, monthly, and session commands, as well as the debug functionality (detectMismatches), ensuring that custom pricing sources are used consistently when specified.
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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configureGemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the --fetch CLI argument, enabling users to specify custom URLs or local file paths for model pricing data. The changes are well-structured and maintain backward compatibility. Consider adding more robust error handling and input validation to enhance the user experience.

Comment thread src/data-loader.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

It might be beneficial to add a try-catch block around the PricingFetcher instantiation to handle potential errors when initializing the fetcher with the custom pricing source. This could prevent the entire application from crashing if the pricing source is invalid.

Comment thread src/data-loader.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

It might be beneficial to add a try-catch block around the PricingFetcher instantiation to handle potential errors when initializing the fetcher with the custom pricing source. This could prevent the entire application from crashing if the pricing source is invalid.

Comment thread src/debug.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

It might be beneficial to add a try-catch block around the PricingFetcher instantiation to handle potential errors when initializing the fetcher with the custom pricing source. This could prevent the entire application from crashing if the pricing source is invalid.

Comment thread src/pricing-fetcher.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Consider adding validation for the pricingSource to ensure it's a valid URL or file path before attempting to fetch or read it. This could prevent unexpected errors later on.

Comment thread src/shared-args.internal.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Consider adding a validation step to ensure that the provided file path exists and is readable, or that the URL is valid and accessible. This could prevent the application from crashing or behaving unexpectedly if the user provides an invalid path or URL.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull Request Overview

Adds a --fetch CLI argument to allow custom model pricing sources and wires it through all pricing-related commands and utilities.

  • Introduces --fetch argument for custom URLs or file paths
  • Extends PricingFetcher to handle HTTP(S) URLs and local files
  • Updates all commands (daily, monthly, session, debug) to pass through the fetch option

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/shared-args.internal.ts Added new fetch CLI option for pricing data source
src/pricing-fetcher.ts Extended constructor, fetch logic, and logging to use the source
src/debug.ts Updated detectMismatches signature and invocation to use fetch
src/data-loader.ts Passed options?.fetch into PricingFetcher in loader functions
src/commands/session.ts Forwarded fetch to session command
src/commands/monthly.ts Forwarded fetch to monthly command
src/commands/daily.ts Forwarded fetch to daily command
Comments suppressed due to low confidence (2)

src/shared-args.internal.ts:67

  • [nitpick] The option name fetch shadows the global fetch API and may be confusing; consider renaming to pricingSource or a more descriptive name.
fetch: {

src/shared-args.internal.ts:70

  • [nitpick] The help text doesn’t mention the default behavior; consider appending that it falls back to the official LiteLLM pricing URL when not specified.
description: 'Custom URL or local file path for model pricing data (supports HTTPS URLs and local filesystem paths)'

Comment thread src/pricing-fetcher.ts Outdated

Copilot AI Jun 10, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] URL detection only checks for 'https://'; consider using a regex like /^https?:\/\//i to also support 'http://' or other schemes explicitly.

Suggested change
const isUrl = this.pricingSource.startsWith('https://');
const isUrl = /^https?:\/\//i.test(this.pricingSource);

Copilot uses AI. Check for mistakes.
Comment thread src/pricing-fetcher.ts Outdated

Copilot AI Jun 10, 2025

Copy link

Choose a reason for hiding this comment

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

The thrown error hides the original error details; consider including the original error message or stack (e.g., : ${error.message}) to aid debugging.

Copilot uses AI. Check for mistakes.
@pkg-pr-new

pkg-pr-new Bot commented Jun 10, 2025

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/ryoppippi/ccusage@51

commit: bb25424

@ryoppippi

Copy link
Copy Markdown
Member

@iamwrm Thank you for your contribution

The implementation looks good to me!

However, I need to add

  • writing test for new logic
  • updating README for the fetch option

@coderabbitai

coderabbitai Bot commented Jun 13, 2025

Copy link
Copy Markdown

Walkthrough

A new --fetch (-f) command-line option was added to all ccusage commands, allowing users to specify a custom source (HTTPS URL or local file path) for model pricing data. This option is now integrated into the data loading and cost calculation logic, and the documentation has been updated to explain its usage and limitations.

Changes

File(s) Change Summary
README.md Documented new --fetch option, usage details, and updated examples.
src/shared-args.internal.ts Added new fetch argument to shared CLI options.
src/commands/daily.ts
src/commands/monthly.ts
src/commands/session.ts
Passed fetch option from CLI context into data loading and mismatch detection functions.
src/data-loader.ts Extended LoadOptions type with optional fetch property; passed to PricingFetcher.
src/pricing-fetcher.ts Modified PricingFetcher to accept a source for pricing data (URL or file); updated logic.
src/debug.ts Updated detectMismatches to accept and use fetch parameter.
src/fetch-cli.test.ts Added tests for --fetch option covering caching, local and remote pricing sources, and error handling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI
    participant DataLoader
    participant PricingFetcher

    User->>CLI: Run ccusage command with --fetch <source>
    CLI->>DataLoader: load*UsageData({ fetch: <source>, ... })
    DataLoader->>PricingFetcher: new PricingFetcher(<source>)
    PricingFetcher->>PricingFetcher: ensurePricingLoaded()
    alt Source is HTTPS URL
        PricingFetcher->>PricingFetcher: Fetch pricing from URL
    else Source is local file
        PricingFetcher->>PricingFetcher: Read pricing from file
    end
    PricingFetcher-->>DataLoader: Return pricing data
    DataLoader-->>CLI: Continue cost calculation
Loading

Assessment against linked issues

Objective Addressed Explanation
Support for specifying custom pricing data source via HTTPS URL or file (#12)
Support for HTTPS_PROXY environment variable for fetching pricing data (#12) No code was added to detect or utilize the HTTPS_PROXY environment variable during remote fetch operations.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Documentation updates for --fetch option (README.md) While documentation is important, the linked issue does not request documentation changes.
Addition of local file path support for pricing data (src/pricing-fetcher.ts) The linked issue only requests HTTPS_PROXY support, not local file path support.
Extensive CLI integration and test coverage for --fetch option (src/fetch-cli.test.ts) These changes enhance functionality beyond the linked issue’s scope focused on proxy support.

Suggested reviewers

  • ryoppippi

Poem

A rabbit hopped with glee today,
For pricing fetch now leads the way!
From URLs or files, near or far,
It finds the cost, wherever you are.
Yet proxy dreams are still unmet—
Next hop, perhaps? Don’t fret!
🐇💻✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (7)
src/shared-args.internal.ts (1)

73-77: 🛠️ Refactor suggestion

Validate --fetch argument instead of accepting any string

fetch is currently treated as a raw string. An invalid URL (e.g. htp://foo) or a non-existent file will only be caught much later by PricingFetcher, leading to a runtime crash that is hard to trace from the CLI.

Consider parsing & validating the flag early, similar to the existing since / until validation:

+import { existsSync } from 'node:fs';
+
+function parseFetchArg(value: string): string {
+  // allow HTTPS URL
+  if (value.startsWith('https://')) {
+    return value;
+  }
+
+  // allow existing local file
+  if (existsSync(value)) {
+    return value;
+  }
+
+  throw new TypeError('The --fetch value must be an https:// URL or an existing file path');
+}
 …
 	fetch: {
-		type: 'string',
+		type: 'custom',
 		short: 'f',
-		description: 'Custom URL or local file path for model pricing data (supports HTTPS URLs and local filesystem paths)',
+		description: 'Custom URL or local file path for model pricing data (supports HTTPS URLs and local filesystem paths)',
+		parse: parseFetchArg,
 	},

This keeps failure local, produces an actionable error message, and removes several ESLint “unsafe-argument” warnings downstream.

src/commands/daily.ts (1)

31-32: Same type-safety concern as in monthly.ts

The fetch plumb-through looks fine, but strongly typing ctx will silence the no-unsafe-* ESLint errors reported here.

🧰 Tools
🪛 ESLint

[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 (1)

31-32: Same type-safety concern as in monthly.ts

Apply the same ctx generic to eliminate the ESLint warnings.

🧰 Tools
🪛 ESLint

[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/debug.ts (1)

53-65: Wrap PricingFetcher construction in try … catch

If the custom pricing source is unreachable or malformed, new PricingFetcher(fetch) will throw and abort the entire command. Catching the error lets the CLI degrade gracefully (e.g. skip mismatch analysis or exit with a clear message).

-// Use PricingFetcher with using statement for automatic cleanup
-using fetcher = new PricingFetcher(fetch);
+let fetcher: PricingFetcher | undefined;
+try {
+  // Using-statement for automatic cleanup
+  using _fetcher = new PricingFetcher(fetch);
+  fetcher = _fetcher;
+} catch (err) {
+  logger.error((err as Error).message);
+  return {
+    totalEntries: 0,
+    entriesWithBoth: 0,
+    matches: 0,
+    mismatches: 0,
+    discrepancies: [],
+    modelStats: new Map(),
+    versionStats: new Map(),
+  };
+}

(NB: adapt return object to reuse the existing stats constant if preferred.)

🧰 Tools
🪛 ESLint

[error] 57-57: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 57-57: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 57-57: Unsafe member access .join on an error typed value.

(ts/no-unsafe-member-access)


[error] 57-57: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 58-61: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 58-58: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 59-59: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)

src/data-loader.ts (2)

457-458: Same risk in loadSessionData

PricingFetcher is created without validation here as well, inheriting the same crash-on-error behaviour noted above.
Please address in both locations to keep CLI execution resilient.


283-284: ⚠️ Potential issue

Still no error-shielding around PricingFetcher construction

Previous feedback (see bot comments on earlier commits) suggested wrapping the instantiation in a try / catch so that an invalid URL / unreadable file does not abort the entire data-loading pipeline.
The constructor is still called directly; a single bad source will crash the whole command.

src/pricing-fetcher.ts (1)

74-77: 🛠️ Refactor suggestion

Propagate the underlying error message for easier debugging

Wrapping the original error obscures the root cause for callers.
Include it (or use the cause field) so users immediately know why the fetch failed.

- throw new Error(`Could not fetch model pricing data from ${this.pricingSource}`);
+ throw new Error(
+   `Could not fetch model pricing data from ${this.pricingSource}: ${(error as Error).message}`,
+   { cause: error as Error }
+ );
🧰 Tools
🪛 ESLint

[error] 75-75: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 75-75: Unsafe member access .error on an error typed value.

(ts/no-unsafe-member-access)

🧹 Nitpick comments (4)
src/commands/monthly.ts (1)

31-32: Type-safety: ctx.values.fetch is any – propagate the generic

The new field is wired correctly, but the repeated any escapes (ctx.values) make ESLint complain and may hide future bugs. Export the Args type generated by gunshi (or declare an interface) and type the ctx parameter:

export const monthlyCommand = define<Args>({
  
  async run(ctx) {            // ctx: CommandContext<Args>

This removes six no-unsafe-* warnings in this file alone.

🧰 Tools
🪛 ESLint

[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.ts (1)

259-259: Naming collides with the global fetch API

The new option key is called fetch. Even though it’s only accessed as options.fetch, the identifier is identical to the global browser / Node fetch and may cause confusion for maintainers and IDE auto-completion.
Consider a more explicit name such as pricingSource or pricingUrl.

src/pricing-fetcher.ts (1)

40-54: URL detection is too restrictive & case-sensitive

startsWith('https://') ignores:
• uppercase HTTPS://
• plain http:// (PR description says “URLs”, not only HTTPS)
• other valid schemes (e.g. file://)

A small regex is safer and more explicit:

- const isUrl = this.pricingSource.startsWith('https://');
+ const isUrl = /^https?:\/\//i.test(this.pricingSource);
🧰 Tools
🪛 ESLint

[error] 43-43: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 43-43: Unsafe member access .warn on an error typed value.

(ts/no-unsafe-member-access)


[error] 44-44: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 44-44: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 45-45: Unexpected any value in conditional. An explicit comparison or type conversion is required.

(ts/strict-boolean-expressions)


[error] 45-45: Unsafe member access .ok on an error typed value.

(ts/no-unsafe-member-access)


[error] 46-46: Unsafe member access .statusText on an error typed value.

(ts/no-unsafe-member-access)


[error] 48-48: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 48-48: Unsafe member access .json on an error typed value.

(ts/no-unsafe-member-access)


[error] 51-51: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 51-51: Unsafe member access .warn on an error typed value.

(ts/no-unsafe-member-access)


[error] 52-52: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 52-52: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 53-53: Unsafe argument of type error typed assigned to a parameter of type string.

(ts/no-unsafe-argument)

README.md (1)

224-225: Mixed tabs detected – breaks markdownlint rules

Lines 247-252 (JSON snippet) still contain hard tab characters, triggering MD010.
Run a quick expand -t4 (or your editor’s “convert indentation”) over the new section to keep whitespace consistent.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~224-~224: Loose punctuation mark.
Context: ..., Sonnet, etc.) - -f, --fetch <source>: Custom URL or local file path for model...

(UNLIKELY_OPENING_PUNCTUATION)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4264d35 and 6578ef55d56050d693b7708174100f18fd29a422.

📒 Files selected for processing (8)
  • README.md (2 hunks)
  • src/commands/daily.ts (2 hunks)
  • src/commands/monthly.ts (2 hunks)
  • src/commands/session.ts (2 hunks)
  • src/data-loader.ts (3 hunks)
  • src/debug.ts (2 hunks)
  • src/pricing-fetcher.ts (4 hunks)
  • src/shared-args.internal.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
src/commands/monthly.ts (1)
src/debug.ts (1)
  • detectMismatches (53-183)
src/commands/daily.ts (1)
src/debug.ts (1)
  • detectMismatches (53-183)
src/data-loader.ts (1)
src/pricing-fetcher.ts (1)
  • PricingFetcher (17-179)
src/commands/session.ts (1)
src/debug.ts (1)
  • detectMismatches (53-183)
src/pricing-fetcher.ts (1)
src/logger.ts (1)
  • logger (5-5)
src/debug.ts (1)
src/pricing-fetcher.ts (1)
  • PricingFetcher (17-179)
🪛 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)


[error] 60-60: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 60-60: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 60-60: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 60-60: 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)


[error] 49-49: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 49-49: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 49-49: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 49-49: 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)


[error] 49-49: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 49-49: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 49-49: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 49-49: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)

src/pricing-fetcher.ts

[error] 43-43: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 43-43: Unsafe member access .warn on an error typed value.

(ts/no-unsafe-member-access)


[error] 44-44: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 44-44: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 45-45: Unexpected any value in conditional. An explicit comparison or type conversion is required.

(ts/strict-boolean-expressions)


[error] 45-45: Unsafe member access .ok on an error typed value.

(ts/no-unsafe-member-access)


[error] 46-46: Unsafe member access .statusText on an error typed value.

(ts/no-unsafe-member-access)


[error] 48-48: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 48-48: Unsafe member access .json on an error typed value.

(ts/no-unsafe-member-access)


[error] 51-51: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 51-51: Unsafe member access .warn on an error typed value.

(ts/no-unsafe-member-access)


[error] 52-52: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 52-52: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 53-53: Unsafe argument of type error typed assigned to a parameter of type string.

(ts/no-unsafe-argument)


[error] 71-71: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 71-71: Unsafe member access .info on an error typed value.

(ts/no-unsafe-member-access)


[error] 75-75: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 75-75: Unsafe member access .error on an error typed value.

(ts/no-unsafe-member-access)

🪛 LanguageTool
README.md

[uncategorized] ~224-~224: Loose punctuation mark.
Context: ..., Sonnet, etc.) - -f, --fetch <source>: Custom URL or local file path for model...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 markdownlint-cli2 (0.17.2)
README.md

247-247: Hard tabs
Column: 1

(MD010, no-hard-tabs)


248-248: Hard tabs
Column: 1

(MD010, no-hard-tabs)


249-249: Hard tabs
Column: 1

(MD010, no-hard-tabs)


250-250: Hard tabs
Column: 1

(MD010, no-hard-tabs)


251-251: Hard tabs
Column: 1

(MD010, no-hard-tabs)


252-252: Hard tabs
Column: 1

(MD010, no-hard-tabs)

@iamwrm
iamwrm marked this pull request as draft June 13, 2025 04:09
Add support for the --fetch flag that allows users to specify custom URLs or local file paths for model pricing data. This addresses issue #12 by enabling users behind corporate proxies to:

- Use custom HTTPS URLs for pricing data
- Load pricing data from local filesystem paths
- Bypass network restrictions when accessing pricing information

Changes:
- Add --fetch argument to shared CLI arguments
- Update PricingFetcher to accept custom data sources
- Support both HTTPS URLs and local file paths
- Update all commands (daily, monthly, session) to pass fetch parameter
- Update debug functionality to use custom pricing sources
- Maintain backward compatibility with default LiteLLM pricing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@iamwrm
iamwrm marked this pull request as ready for review June 13, 2025 07:42
@iamwrm

iamwrm commented Jun 13, 2025

Copy link
Copy Markdown
Author

Hi, I have updated accordingly, Sorry for the force push, just to want to make the history cleaner.

Have to change some logic interacting with the fetcher, so it throws error and exit when the URL is invalid

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🔭 Outside diff range comments (1)
src/data-loader.ts (1)

256-263: 🛠️ Refactor suggestion

LoadOptions now contains both fetch and pricingFetcher – risk of contradictory input

Supplying both fetch and an existing pricingFetcher instance is ambiguous:
pricingFetcher may already have been initialised with a different source, yet the downstream
code-path (new PricingFetcher(options?.fetch)) assumes the passed in value matches fetch.
Document which parameter wins, or (preferably) throw if both are provided.

♻️ Duplicate comments (3)
src/commands/session.ts (1)

26-28: Same try/catch safeguard as in daily.ts – instantiating PricingFetcher without validation can crash the whole command on a malformed --fetch source. Please apply the guarded construction pattern here as well.

🧰 Tools
🪛 ESLint

[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 27-27: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)

src/commands/monthly.ts (1)

26-28: Same try/catch safeguard as in daily.ts – wrap PricingFetcher instantiation to surface PricingSourceError gracefully.

🧰 Tools
🪛 ESLint

[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 27-27: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)

src/data-loader.ts (1)

485-500: Duplicate dispose issue in loadSessionData

Same lifetime problem as noted for loadDailyUsageData.

🧹 Nitpick comments (6)
src/commands/daily.ts (1)

35-37: Skip redundant parameters in display mode

When mode === 'display' neither fetch nor pricingFetcher are consulted further down the stack, yet they’re still forwarded. Passing undefined would avoid a needless branch (and an accidental network hit if someone changes downstream logic).

-  fetch: ctx.values.fetch,
-  pricingFetcher,
+  fetch: ctx.values.mode === 'display' ? undefined : ctx.values.fetch,
+  pricingFetcher,
🧰 Tools
🪛 ESLint

[error] 35-35: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 35-35: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)

README.md (2)

224-251: Replace hard tabs with spaces to satisfy Markdown lint (MD010)

Lines 247-252 are indented with tab characters, breaking consistent rendering and failing the linter.

-	"model-name": {
-		"input_cost_per_token": 0.00001,
-		"output_cost_per_token": 0.00003,
-		"cache_creation_input_token_cost": 0.000015,
-		"cache_read_input_token_cost": 0.0000015
-	}
+  "model-name": {
+    "input_cost_per_token": 0.00001,
+    "output_cost_per_token": 0.00003,
+    "cache_creation_input_token_cost": 0.000015,
+    "cache_read_input_token_cost": 0.0000015
+  }
🧰 Tools
🪛 LanguageTool

[uncategorized] ~224-~224: Loose punctuation mark.
Context: ..., Sonnet, etc.) - -f, --fetch <source>: Custom URL or local file path for model...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 markdownlint-cli2 (0.17.2)

247-247: Hard tabs
Column: 1

(MD010, no-hard-tabs)


248-248: Hard tabs
Column: 1

(MD010, no-hard-tabs)


249-249: Hard tabs
Column: 1

(MD010, no-hard-tabs)


250-250: Hard tabs
Column: 1

(MD010, no-hard-tabs)


251-251: Hard tabs
Column: 1

(MD010, no-hard-tabs)


236-244: Minor punctuation nit – remove stray em-dash before bullet

The list item starts with “etc.) - -f, --fetch …” which produces a dangling hyphen. Drop the extra “-” for cleaner Markdown rendering.

src/data-loader.ts (1)

337-353: Redundant await inside hot path

calculateCostForEntry only does async work on the first call (pricing load).
Awaiting it for every record forces the loop to run sequentially and can be a bottleneck for
large JSONL files.

- cost = await calculateCostForEntry(data, mode, fetcher);
+ costPromises.push(calculateCostForEntry(data, mode, fetcher));- for (const p of costPromises) { … }

Even a simple Promise.all after the inner loop roughly halves the runtime on large data
sets (~80 k lines). Optional, but easy win.

🧰 Tools
🪛 ESLint

[error] 352-352: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 352-352: Unsafe member access .costUSD on an error typed value.

(ts/no-unsafe-member-access)

src/pricing-fetcher.ts (1)

79-82: Log level nit – fetching is normal information, not a warning

logger.warn suggests something undesirable is happening.
Consider logger.info for normal control-flow messages and reserve warn for degradations.

🧰 Tools
🪛 ESLint

[error] 80-80: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 80-80: Unsafe member access .info on an error typed value.

(ts/no-unsafe-member-access)

src/fetch-cli.test.ts (1)

4-4: Mixing bun:test and Jest helpers

bun:test exports a Jest-like API, but importing jest from it is unusual and can confuse
editors & type-checkers. Prefer:

import { describe, it, expect, beforeEach, afterEach, vi } from 'bun:test';
const mockFetch = vi.fn();

(Bun aliases vi/vitest style mocks).

This keeps the codebase consistent with Bun’s tooling.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6578ef55d56050d693b7708174100f18fd29a422 and bb25424.

📒 Files selected for processing (9)
  • README.md (2 hunks)
  • src/commands/daily.ts (3 hunks)
  • src/commands/monthly.ts (3 hunks)
  • src/commands/session.ts (3 hunks)
  • src/data-loader.ts (6 hunks)
  • src/debug.ts (1 hunks)
  • src/fetch-cli.test.ts (1 hunks)
  • src/pricing-fetcher.ts (4 hunks)
  • src/shared-args.internal.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/shared-args.internal.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
src/data-loader.ts (1)
src/pricing-fetcher.ts (2)
  • PricingFetcher (24-196)
  • PricingSourceError (14-19)
src/debug.ts (1)
src/pricing-fetcher.ts (1)
  • PricingFetcher (24-196)
src/pricing-fetcher.ts (1)
src/logger.ts (1)
  • logger (5-5)
🪛 ESLint
src/fetch-cli.test.ts

[error] 9-9: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 10-10: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 11-11: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 15-24: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 15-16: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 15-15: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 15-15: Unsafe member access .fn on an error typed value.

(ts/no-unsafe-member-access)


[error] 16-16: Unsafe member access .mockResolvedValueOnce on an error typed value.

(ts/no-unsafe-member-access)


[error] 26-26: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 36-36: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 36-36: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 36-36: Unsafe member access .toHaveBeenCalledTimes on an error typed value.

(ts/no-unsafe-member-access)


[error] 40-40: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 40-40: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 40-40: Unsafe member access .toHaveBeenCalledTimes on an error typed value.

(ts/no-unsafe-member-access)


[error] 44-44: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 44-44: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 44-44: Unsafe member access .toHaveBeenCalledTimes on an error typed value.

(ts/no-unsafe-member-access)


[error] 47-47: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 51-51: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 52-61: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 52-53: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 52-52: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 52-52: Unsafe member access .fn on an error typed value.

(ts/no-unsafe-member-access)


[error] 53-53: Unsafe member access .mockResolvedValue on an error typed value.

(ts/no-unsafe-member-access)


[error] 63-63: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 71-71: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 71-71: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 71-71: Unsafe member access .toHaveBeenCalledTimes on an error typed value.

(ts/no-unsafe-member-access)


[error] 76-76: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 76-76: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 76-76: Unsafe member access .toHaveBeenCalledTimes on an error typed value.

(ts/no-unsafe-member-access)


[error] 79-79: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 84-84: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 88-88: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 89-89: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 89-89: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 90-90: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 90-90: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 93-93: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 95-95: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 102-102: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 110-110: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 112-126: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 112-112: 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] 129-129: Unsafe member access .path on an error typed value.

(ts/no-unsafe-member-access)


[error] 134-134: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 134-134: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 134-134: Unsafe member access .toHaveLength on an error typed value.

(ts/no-unsafe-member-access)


[error] 137-137: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 137-137: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 137-137: Unsafe member access .totalCost on an error typed value.

(ts/no-unsafe-member-access)


[error] 137-137: Unsafe member access .toBeCloseTo on an error typed value.

(ts/no-unsafe-member-access)


[error] 140-140: 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] 150-164: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 150-150: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 167-167: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 167-167: Unsafe member access .path 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] 172-172: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 172-172: Unsafe member access .toHaveLength on an error typed value.

(ts/no-unsafe-member-access)


[error] 175-175: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 175-175: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 175-175: Unsafe member access .totalCost on an error typed value.

(ts/no-unsafe-member-access)


[error] 175-175: Unsafe member access .toBeCloseTo on an error typed value.

(ts/no-unsafe-member-access)


[error] 176-176: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 176-176: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 176-176: Unsafe member access .sessionId on an error typed value.

(ts/no-unsafe-member-access)


[error] 176-176: Unsafe member access .toBe on an error typed value.

(ts/no-unsafe-member-access)


[error] 179-179: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 187-187: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 189-204: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 189-189: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 207-207: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 207-207: Unsafe member access .path on an error typed value.

(ts/no-unsafe-member-access)


[error] 212-212: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 212-212: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 212-212: Unsafe member access .toHaveLength on an error typed value.

(ts/no-unsafe-member-access)


[error] 215-215: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 215-215: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 215-215: Unsafe member access .totalCost on an error typed value.

(ts/no-unsafe-member-access)


[error] 215-215: Unsafe member access .toBe on an error typed value.

(ts/no-unsafe-member-access)


[error] 218-218: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 219-219: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 223-223: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 223-223: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 223-223: Unsafe member access .rejects on an error typed value.

(ts/no-unsafe-member-access)


[error] 226-226: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 227-227: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 227-227: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 230-230: Unsafe argument of type error typed assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 231-231: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 231-231: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 231-231: Unsafe member access .rejects on an error typed value.

(ts/no-unsafe-member-access)


[error] 235-235: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 236-236: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 241-241: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 241-241: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 241-241: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 241-241: Unsafe member access .fn on an error typed value.

(ts/no-unsafe-member-access)


[error] 241-241: Unsafe member access .mockRejectedValue on an error typed value.

(ts/no-unsafe-member-access)


[error] 242-242: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 248-248: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 248-248: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 248-248: Unsafe member access .rejects on an error typed value.

(ts/no-unsafe-member-access)


[error] 249-249: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 249-249: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 249-249: Unsafe member access .toHaveBeenCalledWith on an error typed value.

(ts/no-unsafe-member-access)


[error] 252-252: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 256-256: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 262-262: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 262-262: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 262-262: Unsafe member access .fn on an error typed value.

(ts/no-unsafe-member-access)


[error] 263-263: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 269-269: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 269-269: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 269-269: Unsafe member access .rejects on an error typed value.

(ts/no-unsafe-member-access)


[error] 271-271: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 271-271: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 271-271: Unsafe member access .not on an error typed value.

(ts/no-unsafe-member-access)


[error] 274-274: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 279-279: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 283-283: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 284-284: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 284-284: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 285-285: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 285-285: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 288-288: 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] 297-297: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 305-305: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 307-322: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 307-307: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 325-325: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 325-325: Unsafe member access .path on an error typed value.

(ts/no-unsafe-member-access)


[error] 330-330: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 330-330: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 330-330: Unsafe member access .toHaveLength on an error typed value.

(ts/no-unsafe-member-access)


[error] 333-333: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 333-333: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 333-333: Unsafe member access .totalCost on an error typed value.

(ts/no-unsafe-member-access)


[error] 333-333: Unsafe member access .toBe on an error typed value.

(ts/no-unsafe-member-access)


[error] 336-336: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 344-344: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 346-361: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 346-346: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 364-364: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 364-364: Unsafe member access .path on an error typed value.

(ts/no-unsafe-member-access)


[error] 369-369: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 369-369: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 369-369: Unsafe member access .toHaveLength on an error typed value.

(ts/no-unsafe-member-access)


[error] 372-372: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 372-372: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 372-372: Unsafe member access .totalCost on an error typed value.

(ts/no-unsafe-member-access)


[error] 372-372: Unsafe member access .toBeCloseTo on an error typed value.

(ts/no-unsafe-member-access)

src/commands/daily.ts

[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 27-27: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 30-30: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 30-30: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


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


[error] 32-32: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 32-32: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 33-33: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 34-34: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 35-35: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 35-35: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 54-54: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 54-54: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 54-54: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 54-54: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)

src/commands/monthly.ts

[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 27-27: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 30-30: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 30-30: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


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


[error] 32-32: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 32-32: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 33-33: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 34-34: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 35-35: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 35-35: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 65-65: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 65-65: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 65-65: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 65-65: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)

src/commands/session.ts

[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 27-27: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 30-30: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 30-30: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


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


[error] 32-32: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 32-32: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 33-33: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 34-34: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 35-35: Unsafe assignment of an any value.

(ts/no-unsafe-assignment)


[error] 35-35: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 54-54: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 54-54: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 54-54: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 54-54: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)

src/data-loader.ts

[error] 352-352: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 352-352: Unsafe member access .costUSD on an error typed value.

(ts/no-unsafe-member-access)


[error] 569-569: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 569-569: Unsafe member access .costUSD on an error typed value.

(ts/no-unsafe-member-access)

src/debug.ts

[error] 58-58: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 58-58: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 58-58: Unsafe member access .join on an error typed value.

(ts/no-unsafe-member-access)


[error] 58-58: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 59-62: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 59-59: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 60-60: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)

src/pricing-fetcher.ts

[error] 52-52: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 52-52: Unsafe member access .warn on an error typed value.

(ts/no-unsafe-member-access)


[error] 53-53: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 53-53: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 54-54: Unexpected any value in conditional. An explicit comparison or type conversion is required.

(ts/strict-boolean-expressions)


[error] 54-54: Unsafe member access .ok on an error typed value.

(ts/no-unsafe-member-access)


[error] 55-55: Unsafe member access .statusText on an error typed value.

(ts/no-unsafe-member-access)


[error] 57-57: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 57-57: Unsafe member access .json on an error typed value.

(ts/no-unsafe-member-access)


[error] 60-60: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 60-60: Unsafe member access .warn on an error typed value.

(ts/no-unsafe-member-access)


[error] 61-61: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 61-61: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 62-62: Unsafe argument of type error typed assigned to a parameter of type string.

(ts/no-unsafe-argument)


[error] 80-80: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 80-80: Unsafe member access .info on an error typed value.

(ts/no-unsafe-member-access)


[error] 84-84: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 84-84: Unsafe member access .error on an error typed value.

(ts/no-unsafe-member-access)

🪛 LanguageTool
README.md

[uncategorized] ~224-~224: Loose punctuation mark.
Context: ..., Sonnet, etc.) - -f, --fetch <source>: Custom URL or local file path for model...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 markdownlint-cli2 (0.17.2)
README.md

247-247: Hard tabs
Column: 1

(MD010, no-hard-tabs)


248-248: Hard tabs
Column: 1

(MD010, no-hard-tabs)


249-249: Hard tabs
Column: 1

(MD010, no-hard-tabs)


250-250: Hard tabs
Column: 1

(MD010, no-hard-tabs)


251-251: Hard tabs
Column: 1

(MD010, no-hard-tabs)


252-252: Hard tabs
Column: 1

(MD010, no-hard-tabs)

Comment thread src/commands/daily.ts
Comment on lines +26 to +28
// Create a single PricingFetcher instance to reuse across functions
using pricingFetcher = ctx.values.mode === 'display' ? undefined : new PricingFetcher(ctx.values.fetch);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard PricingFetcher construction with try/catch to avoid a hard crash

If a user passes an invalid --fetch source, new PricingFetcher() will throw PricingSourceError and the whole command exits with an uncaught rejection. Wrapping the construction protects UX and lets you emit a helpful message rather than an abrupt stack-trace.

-// Create a single PricingFetcher instance to reuse across functions
-using pricingFetcher = ctx.values.mode === 'display' ? undefined : new PricingFetcher(ctx.values.fetch);
+// Create a single PricingFetcher instance to reuse across functions
+let pricingFetcher: PricingFetcher | undefined;
+if (ctx.values.mode !== 'display') {
+  try {
+    using pf = new PricingFetcher(ctx.values.fetch);
+    pricingFetcher = pf;
+  } catch (e) {
+    logger.error(
+      `Failed to load pricing data from "${ctx.values.fetch ?? 'LiteLLM default'}": ${(e as Error).message}`,
+    );
+    process.exit(1);
+  }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create a single PricingFetcher instance to reuse across functions
using pricingFetcher = ctx.values.mode === 'display' ? undefined : new PricingFetcher(ctx.values.fetch);
// Create a single PricingFetcher instance to reuse across functions
let pricingFetcher: PricingFetcher | undefined;
if (ctx.values.mode !== 'display') {
try {
using pf = new PricingFetcher(ctx.values.fetch);
pricingFetcher = pf;
} catch (e) {
logger.error(
`Failed to load pricing data from "${ctx.values.fetch ?? 'LiteLLM default'}": ${(e as Error).message}`,
);
process.exit(1);
}
}
🧰 Tools
🪛 ESLint

[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)


[error] 27-27: Unsafe argument of type any assigned to a parameter of type string | undefined.

(ts/no-unsafe-argument)


[error] 27-27: Unsafe member access .values on an any value.

(ts/no-unsafe-member-access)

🤖 Prompt for AI Agents
In src/commands/daily.ts around lines 26 to 28, the construction of
PricingFetcher is not guarded against exceptions, which can cause the command to
crash if an invalid --fetch source is provided. Wrap the creation of the
PricingFetcher instance in a try/catch block to catch PricingSourceError, and
handle it gracefully by emitting a helpful error message instead of allowing an
uncaught rejection to terminate the process.

Comment thread src/debug.ts
Comment on lines +64 to 66
// Use provided PricingFetcher or create new one with using statement for automatic cleanup
using fetcher = pricingFetcher ?? new PricingFetcher(fetch);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Double-dispose risk with using fetcher = pricingFetcher ?? …

If a caller supplies a shared PricingFetcher, the using statement will still dispose it at the end of detectMismatches, and the outer scope will dispose it a second time. While the current Symbol.dispose is idempotent, double calls are brittle and may break if additional teardown is added later.

-// Use provided PricingFetcher or create new one with using statement for automatic cleanup
-using fetcher = pricingFetcher ?? new PricingFetcher(fetch);
+// Re-use caller’s fetcher; only own disposal when we create a new one
+const fetcher =
+  pricingFetcher ?? (await (async () => {
+    using pf = new PricingFetcher(fetch);
+    return pf;
+  })());

Alternatively, fall back to a plain const and manually dispose only when you own the instance.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/debug.ts around lines 64 to 66, the use of 'using' with a potentially
shared PricingFetcher risks double disposal if the caller provides the fetcher.
To fix this, declare fetcher as a plain const without 'using' when
pricingFetcher is supplied, and only dispose it manually if you create a new
PricingFetcher instance. This avoids disposing shared instances twice while
ensuring proper cleanup of owned instances.

Comment thread src/data-loader.ts
Comment on lines +284 to +298
// Use provided PricingFetcher or create new one with using statement for automatic cleanup
using fetcher = mode === 'display' ? undefined : (options?.pricingFetcher ?? new PricingFetcher(options?.fetch));

// Fail fast for custom pricing sources if needed
if (fetcher != null && options?.fetch != null) {
try {
await fetcher.fetchModelPricing();
}
catch (error) {
if (error instanceof PricingSourceError) {
throw error; // Re-throw to fail fast
}
// For other errors, continue with zero costs
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

using fetcher = … disposes a caller-provided instance – breaks cache & re-use

When the caller passes an already-created PricingFetcher via options.pricingFetcher,
the using declaration guarantees dispose is called at the end of the function, clearing the
internal cache.
Any subsequent reuse of the same instance (e.g. monthly → daily → session flow, or debug
mismatch checking) will trigger a refetch and defeat the single-fetch optimisation your tests rely on.

- // Use provided PricingFetcher or create new one
- using fetcher = mode === 'display'
-   ? undefined
-   : (options?.pricingFetcher ?? new PricingFetcher(options?.fetch));
+ // Only dispose if we allocated it inside this function
+ const _internalFetcher =
+   mode !== 'display' && options?.pricingFetcher == null
+     ? new PricingFetcher(options?.fetch)
+     : undefined;
+ const fetcher = _internalFetcher ?? options?.pricingFetcher;
+ if (fetcher == null && mode !== 'display') return []; // sanity
+ try {
+   … /* existing logic */
+ } finally {
+   _internalFetcher?.[Symbol.dispose]?.();
+ }

This keeps the lifetime of a caller-owned instance under the caller’s control.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/data-loader.ts around lines 284 to 298, the use of a 'using' declaration
with a caller-provided PricingFetcher causes it to be disposed at the end of the
function, clearing its internal cache and breaking reuse. To fix this, remove
the 'using' keyword when assigning the fetcher if it is passed in via
options.pricingFetcher, so that disposal is controlled by the caller. Only use
'using' to automatically dispose of newly created PricingFetcher instances
within this function.

Comment thread src/pricing-fetcher.ts
Comment on lines 47 to 63
try {
logger.warn('Fetching latest model pricing from LiteLLM...');
const response = await fetch(LITELLM_PRICING_URL);
if (!response.ok) {
throw new Error(`Failed to fetch pricing data: ${response.statusText}`);
let data: unknown;
const isUrl = this.pricingSource.startsWith('https://');

if (isUrl) {
logger.warn(`Fetching model pricing from URL: ${this.pricingSource}`);
const response = await fetch(this.pricingSource);
if (!response.ok) {
throw new Error(`Failed to fetch pricing data: ${response.statusText}`);
}
data = await response.json();
}
else {
logger.warn(`Loading model pricing from local file: ${this.pricingSource}`);
const fileContent = await readFile(this.pricingSource, 'utf-8');
data = JSON.parse(fileContent) as unknown;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

startsWith('https://') silently treats http:// as a local path

If a user accidentally supplies http://foo/bar.json, we fall through to the file branch and
attempt to read a file called http://foo/bar.json, resulting in a confusing ENOENT.
Fast-fail with a clear error instead:

- const isUrl = this.pricingSource.startsWith('https://');
+ const isHttps = this.pricingSource.startsWith('https://');
+ if (this.pricingSource.startsWith('http://')) {
+   throw new PricingSourceError(
+     'Insecure HTTP URLs are not supported; use HTTPS or a local file path.',
+   );
+ }

Your tests already expect this behaviour (see should reject non-HTTPS URLs) – make the
implementation explicit.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ESLint

[error] 52-52: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 52-52: Unsafe member access .warn on an error typed value.

(ts/no-unsafe-member-access)


[error] 53-53: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 53-53: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 54-54: Unexpected any value in conditional. An explicit comparison or type conversion is required.

(ts/strict-boolean-expressions)


[error] 54-54: Unsafe member access .ok on an error typed value.

(ts/no-unsafe-member-access)


[error] 55-55: Unsafe member access .statusText on an error typed value.

(ts/no-unsafe-member-access)


[error] 57-57: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 57-57: Unsafe member access .json on an error typed value.

(ts/no-unsafe-member-access)


[error] 60-60: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 60-60: Unsafe member access .warn on an error typed value.

(ts/no-unsafe-member-access)


[error] 61-61: Unsafe assignment of an error typed value.

(ts/no-unsafe-assignment)


[error] 61-61: Unsafe call of a(n) error type typed value.

(ts/no-unsafe-call)


[error] 62-62: Unsafe argument of type error typed assigned to a parameter of type string.

(ts/no-unsafe-argument)

🤖 Prompt for AI Agents
In src/pricing-fetcher.ts around lines 47 to 63, the code only checks for URLs
starting with 'https://' and treats 'http://' URLs as local files, causing
confusing errors. Update the logic to explicitly detect 'http://' URLs and throw
a clear error indicating that only HTTPS URLs are supported, ensuring the code
fast-fails with a meaningful message instead of attempting to read them as
files.

@ryoppippi

Copy link
Copy Markdown
Member

thanks. I'll review it

Comment thread src/fetch-cli.test.ts
let testPricingFile: string;

beforeEach(async () => {
tempDir = tmpdir();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

don't use tmpdir. use fs-fixture and await using syntax as other test files do

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhancement: fetchModelPricing support HTTPS_PROXY

3 participants