Skip to content

Conversation

@konard
Copy link
Contributor

@konard konard commented Jan 11, 2026

Summary

Fixes the [DecimalError] Invalid argument: [object Object] crash in Session.getUsage() when token usage data from AI providers contains unexpected values like objects, NaN, or Infinity instead of numbers.

This issue was encountered when using the agent with certain models (e.g., opencode/grok-code) where the token usage data may contain unexpected structures.

Fixes #119

Root Cause

The getUsage() function was passing raw token values directly to decimal.js constructors, which throws errors when receiving non-numeric data:

[DecimalError] Invalid argument: [object Object]

Solution

Per PR review feedback, implemented two safe conversion functions:

1. toDecimal() - Safe Decimal construction

Uses try-catch for Decimal construction with verbose logging:

export const toDecimal = (value: unknown, context?: string): Decimal => {
  // Log input data in verbose mode
  if (Flag.OPENCODE_VERBOSE) {
    log.debug(() => ({ message: 'toDecimal input', context, ... }));
  }

  try {
    const result = new Decimal(value as any);
    // Log success in verbose mode
    return result;
  } catch (error) {
    // Log error and return Decimal(NaN)
    return new Decimal(NaN);
  }
};

2. toNumber() - Safe number conversion (NEW)

Per latest feedback, renamed safeTokenValue to toNumber with the same pattern as toDecimal:

export const toNumber = (value: unknown, context?: string): number => {
  // Log input data in verbose mode
  if (Flag.OPENCODE_VERBOSE) {
    log.debug(() => ({ message: 'toNumber input', context, ... }));
  }

  try {
    if (value === undefined || value === null) {
      throw new Error(`Cannot convert ${value} to number`);
    }
    const result = Number(value);
    if (Number.isNaN(result)) {
      throw new Error(`Conversion to number resulted in NaN`);
    }
    // Log success in verbose mode
    return result;
  } catch (error) {
    // Log error and return NaN
    return NaN;
  }
};

3. Verbose logging at all stages

When --verbose flag is enabled, detailed logging shows:

  • Input data before conversion
  • Successful conversion results
  • Error details when conversion fails

This helps identify when API response formats change or contain unexpected data structures.

Changes

File Change
js/src/session/index.ts Added toDecimal() with try-catch and verbose logging, renamed safeTokenValue to toNumber with same pattern
js/tests/session-usage.test.ts 48 tests covering toDecimal(), toNumber(), and getUsage() edge cases

Testing

  • 48 unit tests pass covering both safe conversion functions and getUsage() edge cases
  • Tests cover: NaN, Infinity, -Infinity, objects, undefined, null, valid strings, valid data

Test output:

bun test v1.3.5
 48 pass
 0 fail
 104 expect() calls

Related


AI-assisted implementation with verbose debugging support

Adding CLAUDE.md with task information for AI processing.
This file will be removed when the task is complete.

Issue: #119
@konard konard self-assigned this Jan 11, 2026
- Add safe() function to sanitize numeric inputs before passing to Decimal.js
- Wrap all token values with safe() to handle NaN, Infinity, and object values
- Add try-catch around cost calculation to prevent crashes
- Add comprehensive tests for safe() and getUsage() edge cases

Fixes #119
…ins objects

Add safe() wrapper function to sanitize numeric inputs before passing to Decimal.js.
Wrap all token calculations with safe() and add try-catch around cost computation.
This fixes crashes when AI providers return unexpected token usage data.

Resolves #119
Related to upstream anomalyco/opencode#6161
@konard konard marked this pull request as ready for review January 11, 2026 18:22
…rash

Added direct Decimal.js crash demonstration at the start of the experiment
to show the exact error message: [DecimalError] Invalid argument: [object Object]
@konard konard changed the title [WIP] [DecimalError] Invalid argument: getUsage() crashes when token data contains objects fix: [DecimalError] Invalid argument: getUsage() crashes when token data contains objects Jan 11, 2026
konard and others added 2 commits January 11, 2026 19:30
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@konard
Copy link
Contributor Author

konard commented Jan 11, 2026

⚠️ Solution Draft Finished with Errors

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $6.899423 USD
  • Calculated by Anthropic: $0.000000 USD
  • Difference: $-6.899423 (-100.00%)

Note: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.

📎 Log file uploaded as Gist (710KB)
🔗 View complete solution draft log

Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
Copy link
Contributor Author

konard commented Jan 12, 2026

Can we use lodash for that? And, let's also check for similar logic in Rust. Also double check if it possible to get this all tested.

@konard konard marked this pull request as draft January 12, 2026 21:55
@konard
Copy link
Contributor Author

konard commented Jan 12, 2026

🤖 AI Work Session Started

Starting automated work session at 2026-01-12T21:55:18.577Z

The PR has been converted to draft mode while work is in progress.

This comment marks the beginning of an AI work session. Please wait working session to finish, and provide your feedback.

@konard
Copy link
Contributor Author

konard commented Jan 12, 2026

Response to Feedback

Thanks for the review! I've investigated your questions:

1. Can we use lodash for that?

I researched this thoroughly:

Function Behavior Our Need
_.toFinite() Converts InfinityNumber.MAX_VALUE ❌ We need Infinity0
_.isFinite() Same as Number.isFinite() ✅ Works, but...
Number.isFinite() Native JS, no dependency Already using this

Recommendation: lodash's _.toFinite() is not a suitable replacement because it converts Infinity to Number.MAX_VALUE instead of 0. Our safe() function correctly returns 0 for any non-finite value (NaN, Infinity, -Infinity), which is the desired behavior for token counting (we want to ignore invalid values, not use MAX_VALUE).

The current implementation uses native Number.isFinite() which is:

  • Built into JavaScript (no additional dependency)
  • Functionally equivalent to lodash's _.isFinite()
  • More efficient (no function call overhead)

If you prefer using lodash for consistency, I can add lodash.isfinite as a dependency and use it inside the safe() function, but the logic would remain the same.

2. Check for similar logic in Rust

Investigated: The Rust code in rust/src/ is a separate CLI implementation that does not have any token usage or cost calculations. It's currently a stub that handles command-line arguments and echoes messages - it doesn't interact with LLMs or handle token counting. No similar logic exists in Rust that needs protection.

3. Double check if it's possible to get this all tested

Confirmed: All 21 unit tests pass:

Session.safe() - numeric value sanitization: 10 tests
Session.getUsage() - token usage calculation: 11 tests

Tests cover:

  • NaN, Infinity, -Infinity handling
  • Object coercion to NaN
  • Undefined values
  • Division by zero (produces Infinity)
  • Invalid arithmetic (produces NaN)
  • Valid token data
  • Large token counts
  • Anthropic/Bedrock provider metadata

Would you like me to:

  1. Keep the current native Number.isFinite() approach (recommended)
  2. Add lodash.isfinite as a dependency for consistency
  3. Something else?

Please let me know your preference!

@konard konard marked this pull request as ready for review January 12, 2026 22:04
@konard
Copy link
Contributor Author

konard commented Jan 12, 2026

⚠️ Solution Draft Finished with Errors

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $3.558732 USD
  • Calculated by Anthropic: $0.000000 USD
  • Difference: $-3.558732 (-100.00%)

Note: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.

📎 Log file uploaded as Gist (475KB)
🔗 View complete solution draft log

Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
Copy link
Contributor Author

konard commented Jan 12, 2026

Ok, just make me toDecimal function, that use real new Decimal constructor, but for all values, fail with error we should just return new Decimal(NaN).

But we still should print warning to logs in verbose mode to see actual passed object, also add more lazy loging debug output as we do in other files for --verbose mode, so next time it will happen, we will be able to identify the real root cause. Because we actually getting object, which mean our code about getting data from API may be wrong (also double check for sst/opencode repository in that place, may be they have update there in similar place.

I don't like safe function name, so we should not use it as in upstream.

@konard konard marked this pull request as draft January 12, 2026 23:08
@konard
Copy link
Contributor Author

konard commented Jan 12, 2026

🤖 AI Work Session Started

Starting automated work session at 2026-01-12T23:08:45.141Z

The PR has been converted to draft mode while work is in progress.

This comment marks the beginning of an AI work session. Please wait working session to finish, and provide your feedback.

…dling

Per PR review feedback:
- Renamed safe() to toDecimal() as requested
- toDecimal() returns Decimal(NaN) instead of converting to 0
- Added verbose mode logging to show actual values when invalid data received
- Added safeTokenValue() helper for token extraction with debug logging
- Updated tests to verify toDecimal() behavior (returns Decimal(NaN))
- Updated experiment script to demonstrate the new implementation

The verbose logging will help identify the root cause when objects
or other unexpected values are passed from AI provider APIs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@konard konard marked this pull request as ready for review January 12, 2026 23:19
@konard
Copy link
Contributor Author

konard commented Jan 12, 2026

✅ Implementation Complete

All requested changes from feedback have been implemented:

Changes Made

  1. Renamed safe() to toDecimal() - Per your preference to not use the safe function name

  2. toDecimal() returns Decimal(NaN) for invalid values - Instead of converting to 0, the function now returns new Decimal(NaN) which propagates through calculations:

    export const toDecimal = (value: unknown, context?: string): Decimal => {
      if (typeof value !== 'number' || !Number.isFinite(value)) {
        // Log in verbose mode...
        return new Decimal(NaN);
      }
      return new Decimal(value);
    };
  3. Added verbose/debug logging - When --verbose flag is enabled:

    • Logs exact context (field name) where invalid data was received
    • Shows the actual type and value of invalid data (including JSON stringified objects)
    • Uses lazy evaluation callbacks for efficient logging: log.debug(() => ({ ... }))
  4. Checked sst/opencode (now anomalyco/opencode) - Upstream uses a simpler safe() function. Our implementation goes further by returning Decimal(NaN) and adding verbose logging to help identify root causes.

Test Results

bun test v1.3.5
 28 pass
 0 fail
 71 expect() calls

CI Status

All checks passing on ubuntu, macos, and windows.

Ready for your review! 🚀

@konard
Copy link
Contributor Author

konard commented Jan 12, 2026

⚠️ Solution Draft Finished with Errors

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $6.312631 USD
  • Calculated by Anthropic: $0.000000 USD
  • Difference: $-6.312631 (-100.00%)

Note: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.

📎 Log file uploaded as Gist (864KB)
🔗 View complete solution draft log

Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

Now, we should use try catch, because Decimal supports strings as input and more. So only if we get error we return new Decimal(NaN).

if (typeof value !== 'number' || !Number.isFinite(value)) {

That condition should removed. Also double check that we print input data for toDecimal function on all stages, so we can catch the real problem in the future by enabling verbose mode.

@konard konard marked this pull request as draft January 13, 2026 07:45
@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

🤖 AI Work Session Started

Starting automated work session at 2026-01-13T07:45:55.277Z

The PR has been converted to draft mode while work is in progress.

This comment marks the beginning of an AI work session. Please wait working session to finish, and provide your feedback.

Per PR feedback, the toDecimal() function now:
1. Uses try-catch instead of type checking - because Decimal.js
   supports strings and more, so we only return Decimal(NaN) when
   the constructor actually throws an error
2. Logs input data at all stages in verbose mode to help identify
   root causes of invalid data in the future
3. Accepts valid numeric strings like "42" (Decimal.js feature)
4. Properly handles Infinity/-Infinity (Decimal.js accepts them)

Updated tests to reflect the new behavior:
- Numeric strings now create valid Decimals
- Infinity creates Decimal(Infinity), not Decimal(NaN)
- Only values that throw DecimalError become Decimal(NaN)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

Implementation Complete

I've implemented the changes per your feedback:

Changes Made

  1. Replaced type checking with try-catch - The toDecimal() function now uses a try-catch block instead of checking typeof value !== 'number' || !Number.isFinite(value). This allows Decimal.js to handle the conversion natively, which supports:

    • Numbers (finite and infinite)
    • Valid numeric strings like "42" or "3.14"
    • NaN (creates Decimal(NaN))
    • Infinity/-Infinity (creates Decimal(Infinity))
  2. Only return Decimal(NaN) on actual errors - Now we only return new Decimal(NaN) when the Decimal constructor actually throws a [DecimalError], which happens for:

    • Objects: { count: 100 }
    • Arrays: [1, 2, 3]
    • undefined / null
    • Invalid strings: "abc", ""
  3. Verbose logging at all stages - Added three logging points:

    • Input logging before conversion
    • Success logging after conversion
    • Error logging when conversion fails

Test Results

All 29 unit tests pass with the updated behavior.

Ready for your review!

@konard konard marked this pull request as ready for review January 13, 2026 07:56
@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $2.933803 USD
  • Calculated by Anthropic: $1.983857 USD
  • Difference: $-0.949945 (-32.38%)
    📎 Log file uploaded as Gist (452KB)
    🔗 View complete solution draft log

Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

🔄 Auto-restart 1/3

Detected uncommitted changes from previous run. Starting new session to review and commit them.

Uncommitted files:

?? js/fake_image.png

Auto-restart will stop after changes are committed or after 2 more iterations. Please wait until working session will end and give your feedback.

@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

🔄 Auto-restart 2/3

Detected uncommitted changes from previous run. Starting new session to review and commit them.

Uncommitted files:

?? js/fake_image.png

Auto-restart will stop after changes are committed or after 1 more iteration. Please wait until working session will end and give your feedback.

@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $1.505757 USD
  • Calculated by Anthropic: $0.852907 USD
  • Difference: $-0.652850 (-43.36%)
    📎 Log file uploaded as Gist (889KB)
    🔗 View complete solution draft log

Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

safeTokenValue

Function should work exactly the same ways as toDecimal, and should be named toNumber, and if we cannot get a number by parsing and get error that too should be logged. So toNumber handles numbers the same way as toDecimal function, it should return NaN on any error, but that error should be logged on --verbose mode. So we never do have unhandled input values, we must catch them all and never ignore unparsable values/inputs.

@konard konard marked this pull request as draft January 13, 2026 08:08
@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

🤖 AI Work Session Started

Starting automated work session at 2026-01-13T08:08:10.247Z

The PR has been converted to draft mode while work is in progress.

This comment marks the beginning of an AI work session. Please wait working session to finish, and provide your feedback.

Per reviewer feedback:
- Renamed safeTokenValue to toNumber for consistency with toDecimal
- Uses try-catch pattern instead of type checking (like toDecimal)
- Returns NaN on conversion errors (not 0)
- Added verbose logging at all stages (input, success, error)
- Updated getUsage to use safeNum helper for NaN/Infinity handling
- Added 19 new tests for toNumber function

The toNumber function follows the same pattern as toDecimal:
- Logs input in verbose mode
- Attempts conversion with try-catch
- Returns NaN on failure (logged in verbose mode)
- Supports numbers and valid numeric strings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@konard konard marked this pull request as ready for review January 13, 2026 08:32
@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

Implementation Complete

I've implemented all the requested changes:

Changes per feedback

  1. Renamed safeTokenValue to toNumber

    • Function now follows the same pattern as toDecimal
  2. Uses try-catch pattern

    • No more type checking with typeof value !== 'number' || !Number.isFinite(value)
    • Instead, attempts Number(value) conversion and catches any errors
  3. Returns NaN on errors (not 0)

    • When conversion fails, returns NaN
    • The getUsage() function uses a local safeNum helper to convert NaN/Infinity to 0 for calculations
  4. Verbose logging at all stages

    • Logs input data before conversion
    • Logs successful conversion results
    • Logs error details when conversion fails

Test Results

bun test v1.3.5
 48 pass
 0 fail
 104 expect() calls

All 48 tests pass, including 19 new tests for the toNumber function.

CI Status

All CI checks passing on ubuntu, macos, and windows.

Ready for your review!

@konard
Copy link
Contributor Author

konard commented Jan 13, 2026

⚠️ Solution Draft Finished with Errors

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $3.727513 USD
  • Calculated by Anthropic: $0.000000 USD
  • Difference: $-3.727513 (-100.00%)

Note: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.

📎 Log file uploaded as Gist (572KB)
🔗 View complete solution draft log

Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard konard merged commit f87a777 into main Jan 13, 2026
8 checks passed
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.

[DecimalError] Invalid argument: getUsage() crashes when token data contains objects

2 participants