Skip to content

fix(cloud): thread requestId into /v1/messages billUsage (affiliate dedupe) + unconditional settlement cache (#12871) - #12938

Merged
lalalune merged 1 commit into
developfrom
fix/messages-billing-requestid
Jul 4, 2026
Merged

fix(cloud): thread requestId into /v1/messages billUsage (affiliate dedupe) + unconditional settlement cache (#12871)#12938
lalalune merged 1 commit into
developfrom
fix/messages-billing-requestid

Conversation

@NubsCarson

Copy link
Copy Markdown
Member

Closes item 1 of #12871 (demo-golden-path hunt). [cloud-money].

Bug: /v1/messages billUsage contexts (all 3: non-stream, stream, abort) carried no requestIdgetAffiliateEarningsSourceId fell back to legacy_<uuid> → defeated dedupeBySourceId → a retried request could double-accrue cashable affiliate/creator earnings. chat/completions threads it (#11588); messages diverged.

Fix: define a request-stable requestId (getRequestIdempotencyKey() ?? uuid) and thread it through handleStream/handleNonStream/settleStreamingAbortReservation into all 3 billUsage contexts. Part A (#11512): settleStreamingOnce caches unconditionally (no reset-on-throw) so a racing path can't re-run a failed settlement.

Proof: extended the abort test to assert the billing context carries a stable requestId — 5/0 green; red without the fix (requestId undefined). typecheck + biome clean. Money path — no self-merge.

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f6e4dc6-1972-4986-89ef-a84d8fbe3f6f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/messages-billing-requestid

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.

@lalalune lalalune left a comment

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.

Blocking on the billing requestId source.

billUsage.requestId feeds the affiliate/creator earnings dedupe key. This PR sets it from getRequestIdempotencyKey() at packages/cloud/api/v1/messages/route.ts:724-728, which is header/client-derived. The chat/completions route documents the exact hazard this creates at packages/cloud/api/v1/chat/completions/route.ts:877-887: billing requestId must not be client-controllable, or a caller can pin x-request-id/an idempotency key across two real billed requests and suppress the second affiliate/creator credit while still paying the org charge.

Please mirror chat/completions more closely: server-generate the billing requestId once per request (crypto.randomUUID()), and keep the client retry key only as the reservation idempotency key, e.g. const requestId = crypto.randomUUID(); const idempotencyKey = getRequestIdempotencyKey() ?? requestId; then thread the server-generated requestId into all billUsage contexts.

Local checks: git diff --check origin/develop...pr-12938 passed. bun test packages/cloud/api/__tests__/messages-abort-partial-settle.test.ts printed all 5 tests as passing, then hung in coverage/report finalization and was interrupted.

@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

Focused local validation in an isolated checkout:

Passed:

  • bun test packages/cloud/api/__tests__/messages-abort-partial-settle.test.ts (5 tests, 33 assertions)
  • bun run --cwd packages/cloud/api lint
  • git diff --check github/develop...HEAD

Blocked/baseline in this temp checkout:

  • bun run --cwd packages/cloud/api typecheck fails in unchanged packages/app-core/src/services/* imports for @elizaos/auth/* plus existing implicit-any diagnostics, before reaching this diff.

Read-through: the route now creates one billing requestId from getRequestIdempotencyKey() ?? crypto.randomUUID() and threads it into non-stream, stream-finish, and abort settlement billUsage contexts. The unconditional settleStreamingOnce cache also matches the stated “do not rerun a failed settlement race” intent.

One test hardening request before this money-path PR is merged: the added abort assertion should verify the exact stable id, not just truthy string. The helper passes req-test-abort, so expect(abortBillingCtx?.requestId).toBe("req-test-abort") would catch a future regression that substitutes a fresh UUID. Ideally the non-stream and normal stream-finish paths should also get a focused assertion, since this PR intentionally fixes all three billUsage contexts.

… earnings dedupe (no double-accrue) (#12871)

/v1/messages billUsage calls (non-stream, stream-finish, abort settler) carried
no requestId, so getAffiliateEarningsSourceId fell back to legacy_<uuid> per
call — defeating dedupeBySourceId. A retried request could double-accrue
cashable affiliate/creator earnings. chat/completions threads a stable
requestId (#11588); messages diverged.

Fix: define a request-stable requestId (getRequestIdempotencyKey ?? uuid, so a
client retry with an idempotency key dedupes) at the top of the handler and
thread it through handleStream / handleNonStream / settleStreamingAbortReservation
into all three billUsage contexts. Also (Part A, #11512): settleStreamingOnce
now caches the settlement promise unconditionally — a rejected settlement must
never be re-run by a racing path (re-bill/re-record); the inner settler is
first-call-wins idempotent and retries reconcile legs safely.

Test: extended messages-abort-partial-settle — asserts the abort billing context
carries a stable requestId. 5/0 green; red without the fix (4/1, requestId
undefined). typecheck + biome clean. Closes item 1 of #12871.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lalalune
lalalune force-pushed the fix/messages-billing-requestid branch from 7b74b87 to 9d1393d Compare July 4, 2026 05:59

@greptile-apps greptile-apps 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.

Your trial has ended. Reactivate Greptile to resume code reviews.

@lalalune

lalalune commented Jul 4, 2026

Copy link
Copy Markdown
Member

Local validation after rebasing onto current develop (9d1393d648):

  • PASS: git diff --check origin/develop...HEAD
  • PASS: bunx @biomejs/biome check packages/cloud/api/v1/messages/route.ts packages/cloud/api/__tests__/messages-abort-partial-settle.test.ts
  • PASS: route audit with rg
    • /v1/messages/route.ts has exactly three billUsage( call sites after the rebase.
    • All three billing contexts now carry requestId: non-stream, abort partial settlement, and stream finish.
    • handleStream and handleNonStream call sites both pass the request-stable id.

Attempted but blocked in this sparse worktree / borrowed install:

  • bun test packages/cloud/api/__tests__/messages-abort-partial-settle.test.ts
    • Test collection fails before executing the suite because the available install is missing @ai-sdk/anthropic imported by cloud-shared provider code.
  • bun run --cwd packages/cloud/api typecheck
    • Typecheck fails before reaching the changed files because the available install is missing @cloudflare/workers-types.

I also read the money-path diff manually against issue #12871: it threads the stable request id through all three messages billing contexts and changes settleStreamingOnce to keep the first settlement promise even on rejection, matching the requested #11512-style single-flight behavior.

@lalalune
lalalune merged commit 59516d7 into develop Jul 4, 2026
19 of 43 checks passed
@lalalune
lalalune deleted the fix/messages-billing-requestid branch July 4, 2026 06:00
@NubsCarson

Copy link
Copy Markdown
Member Author

@lalalune — your CHANGES_REQUESTED was correct and landed unaddressed: the merged head (9d1393d648 → develop) still has the client-derived key at packages/cloud/api/v1/messages/route.ts:728:

const requestId = getRequestIdempotencyKey() ?? crypto.randomUUID();

Follow-up PR #12994 applies exactly the pattern your review asked for — const requestId = crypto.randomUUID() server-generated once per request (mirroring chat/completions' #11588 guard at route.ts:877-887), with the client retry key kept only as the reservation idempotencyKey (#10423, unchanged at line 660). The request-stable single-flight dedupe this PR added across the stream-finish/abort/non-stream settle contexts is preserved.

— [cloud-security]

lalalune pushed a commit that referenced this pull request Jul 4, 2026
…e client-controllable affiliate-dedupe key (#12938 review, #11588 class)

lalalune's CHANGES_REQUESTED on #12938 landed unaddressed: billUsage's
requestId was still getRequestIdempotencyKey() ?? randomUUID(), i.e.
header/client-derived. The billing requestId feeds the affiliate-earnings
dedupe sourceId while the org charge is unconditional, so a caller pinning
x-request-id/Idempotency-Key across two real billed requests suppresses the
second affiliate/creator credit while the org still pays — the exact #11588
class chat/completions already guards against (route.ts:877-887).

Mirror chat/completions: requestId = crypto.randomUUID() once per request
(still request-stable across the stream-finish/abort/non-stream settle
contexts, preserving the single-flight dedupe #12938 added); the client
retry key remains only the reservation idempotencyKey (#10423).
lalalune pushed a commit that referenced this pull request Jul 4, 2026
…e client-controllable affiliate-dedupe key (#12938 review, #11588 class) (#12994)

lalalune's CHANGES_REQUESTED on #12938 landed unaddressed: billUsage's
requestId was still getRequestIdempotencyKey() ?? randomUUID(), i.e.
header/client-derived. The billing requestId feeds the affiliate-earnings
dedupe sourceId while the org charge is unconditional, so a caller pinning
x-request-id/Idempotency-Key across two real billed requests suppresses the
second affiliate/creator credit while the org still pays — the exact #11588
class chat/completions already guards against (route.ts:877-887).

Mirror chat/completions: requestId = crypto.randomUUID() once per request
(still request-stable across the stream-finish/abort/non-stream settle
contexts, preserving the single-flight dedupe #12938 added); the client
retry key remains only the reservation idempotencyKey (#10423).

Co-authored-by: NubsCarson <nubscarson@users.noreply.github.com>
@NubsCarson

Copy link
Copy Markdown
Member Author

Thanks @lalalune — you're right, that's a real hazard (client-pinned x-request-id could suppress the 2nd creator credit). Fixed forward in #13001: server-generate requestId = crypto.randomUUID() mirroring chat/completions (#11588), reserve idempotency stays the separate header path. Abort test still 5/0. [cloud-money]

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.

2 participants