Skip to content

fix: cap maxTokens in OpenRouter calls to prevent 402 billing errors#133

Open
pritpatel2412 wants to merge 4 commits into
tinyfish-io:mainfrom
pritpatel2412:fix/openrouter-token-limit
Open

fix: cap maxTokens in OpenRouter calls to prevent 402 billing errors#133
pritpatel2412 wants to merge 4 commits into
tinyfish-io:mainfrom
pritpatel2412:fix/openrouter-token-limit

Conversation

@pritpatel2412

Copy link
Copy Markdown

Problem

When running populate and refresh workflows, @mastra/core's Agent class defaults to requesting the model's maximum output token limit (e.g. 65,535 tokens for Gemini 2.5 Flash). On OpenRouter, this causes pre-request credit checks to fail with a 402 Payment Required error if using the free tier or a low-credit wallet, even if the actual generation uses very few tokens.

Changes

  • Introduced a model wrapper proxy (wrapModelWithTokenLimit) in backend/src/mastra/model-wrapper.ts that intercepts and caps maxTokens to a safe limit of 8192.
  • Wrapped all OpenRouter model instances across agents (populate, investigate, refresh), workflows (enumerate step), and pipeline components (inferSchema).
  • Added a unit test suite (backend/src/mastra/model-wrapper.test.ts) using Node's native runner to verify token capping, property forwarding, and binding preservation.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds three major features to the Bigset system. First, it introduces wrapModelWithTokenLimit, a Proxy-based utility that enforces maximum token output limits on OpenRouter models, applied consistently across all agents (investigate, populate, refresh) and pipelines. Second, it implements per-cell provenance tracking throughout the stack: backend tools accept provenance metadata (URL, search query, snippet context) during row insertion/update, the database schema stores this per-column, and the frontend displays it in an expanded cell detail panel with URL link validation. Third, it converts run statistics queries from in-memory collection to cursor-based pagination with configurable limits and supporting database indexes. Additional changes include deferred state updates in row change detection and script maintenance for cross-platform Python compatibility.

Possibly Related PRs

  • tinyfish-io/bigset#115: Extends cell expand side-sheet UI from PR #115 by updating frontend/components/SideSheet.tsx CellDetail and dataset page/table plumbing to pass and render per-cell provenance metadata.
  • tinyfish-io/bigset#26: Builds on AI populate implementation by extending backend/src/mastra/tools/dataset-tools.ts insert_row/update_row with per-cell provenance support and wrapping the populate model with wrapModelWithTokenLimit.
  • tinyfish-io/bigset#81: Both PRs modify backend populate/investigate flow in backend/src/mastra/agents/investigate.ts and populate.ts, with provenance/insertion-instruction updates tied to the investigate-subagent architecture.

Suggested Reviewers

  • simantak-dabhade
  • MMeteorL
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: introducing token limit capping for OpenRouter calls to prevent billing errors.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem being solved and detailing the key changes made across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

…n-limit

# Conflicts:
#	backend/src/mastra/agents/populate.ts

@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

🧹 Nitpick comments (1)
backend/src/mastra/model-wrapper.ts (1)

13-13: 💤 Low value

Consider deep cloning options to prevent shared nested object mutations.

The shallow spread { ...options } only clones the top-level properties. If options contains nested objects (e.g., configuration objects), modifications to those nested properties by the underlying model would affect both the original and modified options objects.

While this is acceptable for the current use case (you're only modifying the primitive maxTokens field), consider using structuredClone(options) for safer isolation:

-      const modifiedOptions = { ...options };
+      const modifiedOptions = structuredClone(options);

This would prevent any potential side effects from nested object mutations.

Also applies to: 24-24

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/mastra/model-wrapper.ts` at line 13, The code currently creates a
shallow clone via "const modifiedOptions = { ...options }" which can allow
nested objects to be shared and mutated; replace this with a deep clone (e.g.,
use structuredClone(options) to produce modifiedOptions) so changes like setting
maxTokens do not mutate nested properties on the original options object; update
the same pattern wherever used (the modifiedOptions variable in this module)
and, if runtime compatibility requires, add a small fallback deep-clone strategy
when structuredClone is unavailable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/convex/runStats.ts`:
- Line 84: Clamp and validate the incoming args.limit (used to compute limit and
a second page-size variable) so negative values and NaN cannot pass:
parse/normalize args.limit to a finite non‑negative integer (e.g., use
Number.isFinite and Math.max(0, Math.floor(Number(args.limit) || 0))) then apply
Math.min(..., MAX_PAGE_SIZE) with DEFAULT_PAGE_SIZE as fallback; update the
computations that set limit and the other page-size variable (the places using
args.limit, DEFAULT_PAGE_SIZE and MAX_PAGE_SIZE) to use this validated positive
safe integer.

In `@scripts/verify-authz.sh`:
- Line 79: The anonymous authz test is failing to exercise the auth check
because the mutation payload for datasets:create is missing the required
maxRowCount field (frontend/convex/datasets.ts validates maxRowCount via
v.number()), so Convex rejects the args before auth runs; update the mutation
call in scripts/verify-authz.sh (the line invoking mutation
'{"path":"datasets:create","args":{...}}') to include a valid numeric
"maxRowCount" (e.g., 100) inside args so the request passes schema validation
and reaches the authentication check that should return "Not authenticated".

---

Nitpick comments:
In `@backend/src/mastra/model-wrapper.ts`:
- Line 13: The code currently creates a shallow clone via "const modifiedOptions
= { ...options }" which can allow nested objects to be shared and mutated;
replace this with a deep clone (e.g., use structuredClone(options) to produce
modifiedOptions) so changes like setting maxTokens do not mutate nested
properties on the original options object; update the same pattern wherever used
(the modifiedOptions variable in this module) and, if runtime compatibility
requires, add a small fallback deep-clone strategy when structuredClone is
unavailable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7189a61f-780f-4663-957e-276578721c40

📥 Commits

Reviewing files that changed from the base of the PR and between 71e32ce and f947df6.

📒 Files selected for processing (17)
  • backend/src/mastra/agents/investigate.ts
  • backend/src/mastra/agents/populate.ts
  • backend/src/mastra/agents/refresh.ts
  • backend/src/mastra/model-wrapper.test.ts
  • backend/src/mastra/model-wrapper.ts
  • backend/src/mastra/tools/dataset-tools.ts
  • backend/src/mastra/workflows/populate.ts
  • backend/src/pipeline/schema-inference.ts
  • frontend/app/dataset/[id]/page.tsx
  • frontend/components/SideSheet.tsx
  • frontend/components/table/DataRow.tsx
  • frontend/components/table/types.ts
  • frontend/components/table/use-row-change-detection.ts
  • frontend/convex/datasetRows.ts
  • frontend/convex/runStats.ts
  • frontend/convex/schema.ts
  • scripts/verify-authz.sh

},
handler: async (ctx, args) => {
const runs = await ctx.db
const limit = Math.min(args.limit ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);

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 | ⚡ Quick win

Validate limit to prevent negative or NaN values.

The current limit clamping allows negative numbers or NaN to pass through (e.g., Math.min(-5, 200) returns -5). Convex's paginate() may not handle these values gracefully.

🛡️ Proposed fix to ensure limit is always a positive safe integer
-    const limit = Math.min(args.limit ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
+    const limit = Math.max(1, Math.min(args.limit ?? DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE));

Apply this change on both Line 84 and Line 110.

Also applies to: 110-110

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/convex/runStats.ts` at line 84, Clamp and validate the incoming
args.limit (used to compute limit and a second page-size variable) so negative
values and NaN cannot pass: parse/normalize args.limit to a finite non‑negative
integer (e.g., use Number.isFinite and Math.max(0, Math.floor(Number(args.limit)
|| 0))) then apply Math.min(..., MAX_PAGE_SIZE) with DEFAULT_PAGE_SIZE as
fallback; update the computations that set limit and the other page-size
variable (the places using args.limit, DEFAULT_PAGE_SIZE and MAX_PAGE_SIZE) to
use this validated positive safe integer.

Comment thread scripts/verify-authz.sh
"$(query '{"path":"datasets:listMine","args":{},"format":"json"}' | assert_error_contains 'Not authenticated')"
run_test "anon datasets.create -> Not authenticated" \
"$(mutation '{"path":"datasets:create","args":{"name":"x","description":"x","cadence":"daily","columns":[]},"format":"json"}' | assert_error_contains 'Not authenticated')"
"$(mutation '{"path":"datasets:create","args":{"name":"x","description":"x","refreshCadence":"daily","columns":[]},"format":"json"}' | assert_error_contains 'Not authenticated')"

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that the anonymous datasets.create test produces "Not authenticated" error

CONVEX="${CONVEX_URL:-http://localhost:3210}"

echo "Testing anonymous datasets.create with current payload..."
RESULT=$(curl -s "$CONVEX/api/mutation" -X POST \
  -H 'Content-Type: application/json' \
  -d '{"path":"datasets:create","args":{"name":"x","description":"x","refreshCadence":"daily","columns":[]},"format":"json"}')

echo "Response: $RESULT"

# Check if error message contains "Not authenticated"
if echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); exit(0 if 'Not authenticated' in d.get('errorMessage','') else 1)"; then
  echo "✓ Test passes - returns 'Not authenticated' as expected"
else
  echo "✗ Test may fail - check if error is about missing maxRowCount instead"
  echo ""
  echo "If validation fails, update line 79 to include maxRowCount:"
  echo '  {"path":"datasets:create","args":{"name":"x","description":"x","refreshCadence":"daily","maxRowCount":100,"columns":[]},"format":"json"}'
fi

Repository: tinyfish-io/bigset

Length of output: 1141


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show scripts/verify-authz.sh around the referenced line 79
echo "---- scripts/verify-authz.sh: around line 79 ----"
if [ -f scripts/verify-authz.sh ]; then
  sed -n '60,95p' scripts/verify-authz.sh | nl -ba
else
  echo "File scripts/verify-authz.sh not found"
fi

# 2) Locate the datasets:create / datasets.create mutation definition and show the args validator
echo
echo "---- Search for 'datasets:create' and 'maxRowCount' in datasets mutation code ----"
# Prefer file name datasets.ts; but search repo to be safe
rg -n --hidden --no-ignore-vcs "datasets:create|datasets\.create" .

rg -n --hidden --no-ignore-vcs "maxRowCount" frontend/convex || true
rg -n --hidden --no-ignore-vcs "maxRowCount" . || true

Repository: tinyfish-io/bigset

Length of output: 173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- scripts/verify-authz.sh existence ----"
ls -la scripts/verify-authz.sh || true

echo
echo "---- scripts/verify-authz.sh: around line 79 (show with line numbers) ----"
if [ -f scripts/verify-authz.sh ]; then
  # Use cat -n (available) instead of nl
  sed -n '70,90p' scripts/verify-authz.sh | cat -n
fi

echo
echo "---- Search for datasets:create / datasets.create ----"
rg -n --hidden --no-ignore-vcs "datasets:create|datasets\.create" .

echo
echo "---- Search for maxRowCount ----"
rg -n --hidden --no-ignore-vcs "maxRowCount" .

Repository: tinyfish-io/bigset

Length of output: 10470


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- frontend/convex/datasets.ts around datasets.create (maxRowCount v.number()) ----"
sed -n '340,470p' frontend/convex/datasets.ts | cat -n

echo
echo "---- frontend/convex/schema.ts around maxRowCount ----"
sed -n '1,120p' frontend/convex/schema.ts | cat -n

echo
echo "---- find auth / Not authenticated usage in datasets.ts ----"
rg -n --hidden --no-ignore-vcs "require.*auth|Not authenticated|unauth" frontend/convex/datasets.ts

echo
echo "---- inspect convex quota pre-flight / requireQuotaRemaining usage in datasets.ts ----"
rg -n --hidden --no-ignore-vcs "requireQuotaRemaining|quota" frontend/convex/datasets.ts frontend/convex/lib/quota.ts

Repository: tinyfish-io/bigset

Length of output: 10580


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --hidden --no-ignore-vcs "DEFAULT_MAX_ROW_COUNT|FREE_TIER_MONTHLY_QUOTA" frontend/convex/datasets.ts frontend/convex/lib/quota.ts frontend/convex/*.ts

Repository: tinyfish-io/bigset

Length of output: 1533


🌐 Web query:

Convex validate mutation args before handler is called order of validation vs handler execution

💡 Result:

In Convex, argument validation is performed before the mutation (handler) is invoked. If the client passes arguments that don’t match the args validator, Convex throws an error rather than calling the handler with bad inputs.[1][2] Code-path evidence: Convex’s server implementation calls a generic invokeFunction(...), then runs validateReturnValue(result) afterwards for return validation.[3][4]. The presence of explicit return validation happening after the handler return strongly implies argument validation happens in (or before) the invokeFunction step, i.e., prior to handler execution. Additionally, Convex’s docs describe validation as ensuring functions are “called with the correct types of arguments” and that supplying unexpected arguments causes an error.[1][5] So the effective order is: 1) Validate input arguments against the args validators. 2) If valid, call your handler(ctx, args). 3) After the handler returns, validate the return value against returns (if provided).[1][3][4] If you want, share the exact Convex function definition (query/mutation/action) and your args/returns setup and I can map it to this order more concretely.

Citations:


Fix anonymous datasets:create authz test payload to include required maxRowCount (scripts/verify-authz.sh line 79)

datasets.create requires maxRowCount: v.number() in frontend/convex/datasets.ts, and Convex validates mutation args before running the handler—so this request won’t reach the auth check that would produce “Not authenticated”. Include a valid maxRowCount so the test actually exercises authentication.

Update payload
 run_test "anon datasets.create -> Not authenticated" \
-  "$(mutation '{"path":"datasets:create","args":{"name":"x","description":"x","refreshCadence":"daily","columns":[]},"format":"json"}' | assert_error_contains 'Not authenticated')"
+  "$(mutation '{"path":"datasets:create","args":{"name":"x","description":"x","refreshCadence":"daily","maxRowCount":100,"columns":[]},"format":"json"}' | assert_error_contains 'Not authenticated')"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-authz.sh` at line 79, The anonymous authz test is failing to
exercise the auth check because the mutation payload for datasets:create is
missing the required maxRowCount field (frontend/convex/datasets.ts validates
maxRowCount via v.number()), so Convex rejects the args before auth runs; update
the mutation call in scripts/verify-authz.sh (the line invoking mutation
'{"path":"datasets:create","args":{...}}') to include a valid numeric
"maxRowCount" (e.g., 100) inside args so the request passes schema validation
and reaches the authentication check that should return "Not authenticated".

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