Skip to content

fix(chat): improve reasoning token estimation logic - #1155

Merged
steebchen merged 2 commits into
mainfrom
fix/chat-reason-est
Nov 15, 2025
Merged

steebchen merged 2 commits into
mainfrom
fix/chat-reason-est

Conversation

@steebchen

@steebchen steebchen commented Nov 15, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • Bug Fixes
    • Improved accuracy of reasoning token estimation for both streaming and non‑streaming operations when explicit token counts are missing.
    • Usage logging now consistently reports calculated reasoning token values instead of raw inputs.
    • Final cost and usage records propagate estimated reasoning tokens across streaming, non‑streaming, and cache paths.
    • Maintained robust fallbacks and error handling for encoding/estimation failures.

@coderabbitai

coderabbitai Bot commented Nov 15, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds logic to compute reasoning token counts when reasoning content exists but explicit reasoningTokens are missing. Applies encoding-based token measurement with a fallback estimate on failure, and propagates the calculated value into streaming and non‑streaming usage logging, final usage chunks, and cost calculations.

Changes

Cohort / File(s) Summary
Reasoning token estimation & usage propagation
apps/gateway/src/chat/chat.ts
When reasoning content exists but reasoningTokens is absent, encode the content to compute calculatedReasoningTokens; on encoding failure, fall back to a content-based estimate. Use calculatedReasoningTokens in streaming and non-streaming paths for final usage chunks, logging entries, and cost calculations; preserve existing fallbacks and error handling.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ChatHandler
    participant TokenEncoder
    participant Logger
    participant CostCalc
    Note over ChatHandler: Incoming request (streaming or non-streaming)
    Client->>ChatHandler: send chat request (may include reasoningContent, reasoningTokens?)
    alt reasoningTokens present
        ChatHandler->>Logger: use provided reasoningTokens
        ChatHandler->>CostCalc: compute costs with provided reasoningTokens
    else reasoningContent present and reasoningTokens missing
        ChatHandler->>TokenEncoder: encode reasoningContent
        alt encode succeeds
            TokenEncoder-->>ChatHandler: tokenCount
        else encode fails
            TokenEncoder-->>ChatHandler: error -> fallbackEstimate
        end
        ChatHandler->>Logger: log calculatedReasoningTokens
        ChatHandler->>CostCalc: compute costs with calculatedReasoningTokens
    end
    alt streaming
        Note over ChatHandler: propagate calculatedReasoningTokens into stream final usage chunk and log
    else non-streaming
        Note over ChatHandler: include calculatedReasoningTokens in final response logging
    end
    ChatHandler-->>Client: stream/return response
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Single-file change with repeated, consistent edits to token estimation and logging paths.
  • Areas to check:
    • Correct handling of empty/absent reasoning content.
    • Proper fallback behavior when encoding fails.
    • Consistent use of calculatedReasoningTokens across streaming final chunks, non-streaming logs, and cost computations.

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: improving reasoning token estimation logic in the chat module, which directly aligns with the summary of changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/chat-reason-est

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/gateway/src/chat/chat.ts (2)

3065-3077: Use calculated reasoning tokens for cost calculation.

The calculateCosts function is called with reasoningTokens (line 3076), but the log insertion uses calculatedReasoningTokens (line 3149). When reasoning tokens are calculated from content rather than provided by the API, the cost calculation will miss reasoning token costs, leading to inaccurate billing.

Apply this diff:

 const costs = calculateCosts(
   usedModel,
   usedProvider,
   calculatedPromptTokens,
   calculatedCompletionTokens,
   cachedTokens,
   {
     prompt: messages.map((m) => m.content).join("\n"),
     completion: fullContent,
     toolResults: streamingToolCalls || undefined,
   },
-  reasoningTokens,
+  calculatedReasoningTokens,
 );

3614-3626: Use calculated reasoning tokens for cost calculation.

The calculateCosts function is called with reasoningTokens (line 3625), but the log insertion uses calculatedReasoningTokens (line 3715). When reasoning tokens are calculated from content rather than provided by the API, the cost calculation will miss reasoning token costs, leading to inaccurate billing.

Apply this diff:

 const costs = calculateCosts(
   usedModel,
   usedProvider,
   calculatedPromptTokens,
   calculatedCompletionTokens,
   cachedTokens,
   {
     prompt: messages.map((m) => m.content).join("\n"),
     completion: content,
     toolResults: toolResults,
   },
-  reasoningTokens,
+  calculatedReasoningTokens,
 );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5662912 and baf60fb.

📒 Files selected for processing (1)
  • apps/gateway/src/chat/chat.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/gateway/src/chat/chat.ts (1)
packages/logger/src/index.ts (2)
  • error (153-160)
  • logger (181-181)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: generate / run
  • GitHub Check: lint / run
  • GitHub Check: e2e-shards (2)
  • GitHub Check: test / run
  • GitHub Check: build / run
  • GitHub Check: e2e-shards (1)
  • GitHub Check: autofix

@steebchen
steebchen enabled auto-merge November 15, 2025 12:58

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/gateway/src/chat/chat.ts (2)

3063-3075: Critical: Use calculated reasoning tokens for cost calculation.

The calculateCosts call uses reasoningTokens (the original value from the provider) instead of calculatedReasoningTokens (the estimated value). This means the reasoning token estimation logic added in this PR won't affect cost calculations, which is likely incorrect.

Apply this diff:

 const costs = calculateCosts(
   usedModel,
   usedProvider,
   calculatedPromptTokens,
   calculatedCompletionTokens,
   cachedTokens,
   {
     prompt: messages.map((m) => m.content).join("\n"),
     completion: fullContent,
     toolResults: streamingToolCalls || undefined,
   },
-  reasoningTokens,
+  calculatedReasoningTokens,
 );

3610-3622: Critical: Use calculated reasoning tokens for cost calculation.

Same issue as in the streaming path: calculateCosts uses reasoningTokens instead of calculatedReasoningTokens. The estimated reasoning tokens won't affect cost calculations.

Apply this diff:

 const costs = calculateCosts(
   usedModel,
   usedProvider,
   calculatedPromptTokens,
   calculatedCompletionTokens,
   cachedTokens,
   {
     prompt: messages.map((m) => m.content).join("\n"),
     completion: content,
     toolResults: toolResults,
   },
-  reasoningTokens,
+  calculatedReasoningTokens,
 );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between baf60fb and 2e97acc.

📒 Files selected for processing (1)
  • apps/gateway/src/chat/chat.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/gateway/src/chat/chat.ts (1)
packages/logger/src/index.ts (2)
  • error (153-160)
  • logger (181-181)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: build / run
  • GitHub Check: test / run
  • GitHub Check: lint / run
  • GitHub Check: generate / run
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: autofix

Comment on lines +2917 to +2931
// Estimate reasoning tokens if not provided but reasoning content exists
let calculatedReasoningTokens = reasoningTokens;
if (!reasoningTokens && fullReasoningContent) {
try {
calculatedReasoningTokens = encode(fullReasoningContent).length;
} catch (error) {
// Fallback to simple estimation if encoding fails
logger.error(
"Failed to encode reasoning text in streaming",
error instanceof Error ? error : new Error(String(error)),
);
calculatedReasoningTokens =
estimateTokensFromContent(fullReasoningContent);
}
}

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.

⚠️ Potential issue | 🟡 Minor

Use explicit null check to preserve provider-supplied zero values.

The condition !reasoningTokens evaluates to true when reasoningTokens is 0, which means the code will recalculate even if a provider explicitly returns 0 to indicate no reasoning tokens were used.

Apply this diff to use a more explicit check:

 // Estimate reasoning tokens if not provided but reasoning content exists
 let calculatedReasoningTokens = reasoningTokens;
-if (!reasoningTokens && fullReasoningContent) {
+if (reasoningTokens == null && fullReasoningContent) {
   try {
     calculatedReasoningTokens = encode(fullReasoningContent).length;
   } catch (error) {
     // Fallback to simple estimation if encoding fails
     logger.error(
       "Failed to encode reasoning text in streaming",
       error instanceof Error ? error : new Error(String(error)),
     );
     calculatedReasoningTokens =
       estimateTokensFromContent(fullReasoningContent);
   }
 }
📝 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
// Estimate reasoning tokens if not provided but reasoning content exists
let calculatedReasoningTokens = reasoningTokens;
if (!reasoningTokens && fullReasoningContent) {
try {
calculatedReasoningTokens = encode(fullReasoningContent).length;
} catch (error) {
// Fallback to simple estimation if encoding fails
logger.error(
"Failed to encode reasoning text in streaming",
error instanceof Error ? error : new Error(String(error)),
);
calculatedReasoningTokens =
estimateTokensFromContent(fullReasoningContent);
}
}
// Estimate reasoning tokens if not provided but reasoning content exists
let calculatedReasoningTokens = reasoningTokens;
if (reasoningTokens == null && fullReasoningContent) {
try {
calculatedReasoningTokens = encode(fullReasoningContent).length;
} catch (error) {
// Fallback to simple estimation if encoding fails
logger.error(
"Failed to encode reasoning text in streaming",
error instanceof Error ? error : new Error(String(error)),
);
calculatedReasoningTokens =
estimateTokensFromContent(fullReasoningContent);
}
}
🤖 Prompt for AI Agents
In apps/gateway/src/chat/chat.ts around lines 2917 to 2931, the check `if
(!reasoningTokens && fullReasoningContent)` treats a provider-supplied 0 as
missing and will recalculate; change the condition to explicitly test for
null/undefined (e.g., reasoningTokens === null || reasoningTokens === undefined)
combined with fullReasoningContent so that an explicit zero is preserved, then
proceed with the existing try/catch encoding and fallback estimation logic.

Comment on lines +3596 to +3609
// Estimate reasoning tokens if not provided but reasoning content exists
let calculatedReasoningTokens = reasoningTokens;
if (!reasoningTokens && reasoningContent) {
try {
calculatedReasoningTokens = encode(reasoningContent).length;
} catch (error) {
// Fallback to simple estimation if encoding fails
logger.error(
"Failed to encode reasoning text",
error instanceof Error ? error : new Error(String(error)),
);
calculatedReasoningTokens = estimateTokensFromContent(reasoningContent);
}
}

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.

⚠️ Potential issue | 🟡 Minor

Use explicit null check to preserve provider-supplied zero values.

Same issue as in the streaming path: the condition !reasoningTokens will be true when reasoningTokens is 0, causing recalculation even when a provider explicitly returns 0.

Apply this diff:

 // Estimate reasoning tokens if not provided but reasoning content exists
 let calculatedReasoningTokens = reasoningTokens;
-if (!reasoningTokens && reasoningContent) {
+if (reasoningTokens == null && reasoningContent) {
   try {
     calculatedReasoningTokens = encode(reasoningContent).length;
   } catch (error) {
     // Fallback to simple estimation if encoding fails
     logger.error(
       "Failed to encode reasoning text",
       error instanceof Error ? error : new Error(String(error)),
     );
     calculatedReasoningTokens = estimateTokensFromContent(reasoningContent);
   }
 }
📝 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
// Estimate reasoning tokens if not provided but reasoning content exists
let calculatedReasoningTokens = reasoningTokens;
if (!reasoningTokens && reasoningContent) {
try {
calculatedReasoningTokens = encode(reasoningContent).length;
} catch (error) {
// Fallback to simple estimation if encoding fails
logger.error(
"Failed to encode reasoning text",
error instanceof Error ? error : new Error(String(error)),
);
calculatedReasoningTokens = estimateTokensFromContent(reasoningContent);
}
}
// Estimate reasoning tokens if not provided but reasoning content exists
let calculatedReasoningTokens = reasoningTokens;
if (reasoningTokens == null && reasoningContent) {
try {
calculatedReasoningTokens = encode(reasoningContent).length;
} catch (error) {
// Fallback to simple estimation if encoding fails
logger.error(
"Failed to encode reasoning text",
error instanceof Error ? error : new Error(String(error)),
);
calculatedReasoningTokens = estimateTokensFromContent(reasoningContent);
}
}
🤖 Prompt for AI Agents
In apps/gateway/src/chat/chat.ts around lines 3596 to 3609, the code uses a
falsy check (`!reasoningTokens`) which treats a valid provider-supplied zero as
missing; change the condition to an explicit null/undefined check (e.g.,
`reasoningTokens == null` or `reasoningTokens === undefined || reasoningTokens
=== null`) so zero is preserved, keep the existing try/catch and fallback logic
intact, and mirror the same explicit check used in the streaming path if
present.

@steebchen
steebchen added this pull request to the merge queue Nov 15, 2025
Merged via the queue into main with commit dc2bbce Nov 15, 2025
14 checks passed
@steebchen
steebchen deleted the fix/chat-reason-est branch November 15, 2025 13:06
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.

1 participant