Uh oh!
There was an error while loading. Please reload this page.
feat(music): pass-through pricing, charged on the audio fal produced - #853
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Warning Review limit reachedNext included review available in 40 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (3)
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. Comment |
There was a problem hiding this comment.
4 issues found across 5 files
Confidence score: 3/5
app/workflows/music/musicGenerationWorkflow.tsrecomputes the deduction fromparams.durationinstead of durableparams.creditsToCharge, so pricing changes during an in-flight run can charge more than the caller was quoted; use the persisted quote for deduction.lib/music/creditCostForDuration.tsincludes a comment treating output-second billing as confirmed even though this helper cannot establish fal’s invoice basis, which could mislead future pricing decisions; qualify or remove the claim.lib/music/creditCostForDuration.tsunderstates the fractional-duration upper bound as$0.008, while the calculation can add nearly one full credit and approach$0.01; correct the documentation or enforce the stated bound.app/workflows/music/musicGenerationWorkflow.tsexceeds the stated 100-line maintainability limit, making the billing flow harder to isolate and review; extract the billing/deduction block or split the workflow.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/workflows/music/musicGenerationWorkflow.ts">
<violation number="1" location="app/workflows/music/musicGenerationWorkflow.ts:75">
P2: When pricing constants change while a run is in flight, this call recomputes the quote from `params.duration` instead of applying durable `params.creditsToCharge`, so the deduction can exceed the caller's quote. Cap the computed actual charge with `params.creditsToCharge` or pass the quoted value into the helper.</violation>
<violation number="2" location="app/workflows/music/musicGenerationWorkflow.ts:75">
P2: Custom agent: **Enforce Clear Code Style and Maintainability Practices**
This workflow is 108 lines, violating Rule 3's explicit under-100-line limit. Extract the billing/deduction block or otherwise split the workflow so the file stays below 100 lines.</violation>
</file>
<file name="lib/music/creditCostForDuration.ts">
<violation number="1" location="lib/music/creditCostForDuration.ts:4">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**
The added comment presents output-second billing as confirmed, but this helper only prices its input and cannot establish fal’s invoice basis. The PR still identifies invoice confirmation as outstanding, so document this as an assumption until verified.</violation>
<violation number="2" location="lib/music/creditCostForDuration.ts:22">
P3: For fractional durations, the documented `$0.008` maximum is incorrect. `creditCostForDuration` can add almost one full credit, so document the bound as less than `$0.01` (or derive it from an explicitly enforced duration precision).</violation>
</file>
Architecture diagram
sequenceDiagram
participant API as API / Gate
participant WF as musicGenerationWorkflow
participant Fal as Fal.ai (External API)
participant Calc as Pricing Logic
participant DB as Ledger / DB
Note over API,DB: Pre-flight: User is gated based on Requested Duration (Quote)
API->>WF: start(requestedDuration, quotedCredits)
WF->>Fal: Request generation (requestedDuration)
Fal-->>WF: Return result (audioUrl, actualSeconds)
WF->>Calc: NEW: creditsForCompletedGeneration(requested, actual)
rect rgb(240, 240, 240)
Note over Calc: Logic: 0.2 credits/sec (CHANGED: no floor)
alt actualSeconds is valid
Calc->>Calc: Calculate credits for actualSeconds
opt actualSeconds > requestedSeconds
Calc->>Calc: NEW: Cap at quotedCredits
end
else actualSeconds is missing or 0
Calc->>Calc: NEW: Fallback to quotedCredits
end
end
Calc-->>WF: finalCreditsToDeduct
WF->>DB: CHANGED: recordCreditDeduction(finalCreditsToDeduct)
Note right of DB: Deduction is now often < Quote
DB-->>WF: Success
WF-->>API: Completion status
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| * fal charges $0.002 per output second and 1 credit is $0.01, so 0.2 credits | ||
| * per second is fal's rate exactly — no markup (recoupable/chat#1999). |
There was a problem hiding this comment.
P1: Custom agent: Flag AI Slop and Fabricated Changes
The added comment presents output-second billing as confirmed, but this helper only prices its input and cannot establish fal’s invoice basis. The PR still identifies invoice confirmation as outstanding, so document this as an assumption until verified.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/music/creditCostForDuration.ts, line 4:
<comment>The added comment presents output-second billing as confirmed, but this helper only prices its input and cannot establish fal’s invoice basis. The PR still identifies invoice confirmation as outstanding, so document this as an assumption until verified.</comment>
<file context>
@@ -1,27 +1,36 @@
- * is not a tenth as expensive to serve as a 100-second one.
+ * Credits per second of audio.
+ *
+ * fal charges $0.002 per output second and 1 credit is $0.01, so 0.2 credits
+ * per second is fal's rate exactly — no markup (recoupable/chat#1999).
*/
</file context>
| *falcharges$0.002peroutputsecondand1creditis$0.01,so0.2credits | |
| *persecondisfal's rate exactly — no markup (recoupable/chat#1999). | |
| *Assumesfalcharges$0.002peroutputsecondand1creditis$0.01;confirmthisagainstfalinvoicesbeforecalling0.2creditspersecondfal's exact rate. |
| const creditsToDeduct = creditsForCompletedGeneration({ | ||
| requestedSeconds: params.duration, | ||
| actualSeconds: result.durationSeconds, | ||
| }); |
There was a problem hiding this comment.
P2: When pricing constants change while a run is in flight, this call recomputes the quote from params.duration instead of applying durable params.creditsToCharge, so the deduction can exceed the caller's quote. Cap the computed actual charge with params.creditsToCharge or pass the quoted value into the helper.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/workflows/music/musicGenerationWorkflow.ts, line 75:
<comment>When pricing constants change while a run is in flight, this call recomputes the quote from `params.duration` instead of applying durable `params.creditsToCharge`, so the deduction can exceed the caller's quote. Cap the computed actual charge with `params.creditsToCharge` or pass the quoted value into the helper.</comment>
<file context>
@@ -68,10 +69,18 @@ export async function musicGenerationWorkflow(generationId: string, params: Musi
+ // Charge for the audio fal actually produced, not the length requested.
+ // `params.creditsToCharge` is what the caller was gated and quoted on; it
+ // stays the ceiling, so the deduction only ever moves in their favour.
+ const creditsToDeduct = creditsForCompletedGeneration({
+ requestedSeconds: params.duration,
+ actualSeconds: result.durationSeconds,
</file context>
| constcreditsToDeduct=creditsForCompletedGeneration({ | |
| requestedSeconds: params.duration, | |
| actualSeconds: result.durationSeconds, | |
| }); | |
| constcreditsToDeduct=Math.min( | |
| params.creditsToCharge, | |
| creditsForCompletedGeneration({ | |
| requestedSeconds: params.duration, | |
| actualSeconds: result.durationSeconds, | |
| }), | |
| ); |
| @@ -6,6 +6,7 @@ import { pollMusicGenerationStep } from "@/app/workflows/music/pollMusicGenerati | |||
| import { fetchMusicResultStep } from "@/app/workflows/music/fetchMusicResultStep"; | |||
There was a problem hiding this comment.
P2: Custom agent: Enforce Clear Code Style and Maintainability Practices
This workflow is 108 lines, violating Rule 3's explicit under-100-line limit. Extract the billing/deduction block or otherwise split the workflow so the file stays below 100 lines.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/workflows/music/musicGenerationWorkflow.ts, line 75:
<comment>This workflow is 108 lines, violating Rule 3's explicit under-100-line limit. Extract the billing/deduction block or otherwise split the workflow so the file stays below 100 lines.</comment>
<file context>
@@ -68,10 +69,18 @@ export async function musicGenerationWorkflow(generationId: string, params: Musi
+ // Charge for the audio fal actually produced, not the length requested.
+ // `params.creditsToCharge` is what the caller was gated and quoted on; it
+ // stays the ceiling, so the deduction only ever moves in their favour.
+ const creditsToDeduct = creditsForCompletedGeneration({
+ requestedSeconds: params.duration,
+ actualSeconds: result.durationSeconds,
</file context>
| * @returns Whole credits to gate on and, once the song lands, to deduct. | ||
| * Rounds up because the ledger is integer cents: one credit buys exactly five | ||
| * seconds at fal's rate, so durations that are not multiples of five round to | ||
| * the next credit. The overcharge is at most $0.008 and is an artifact of the |
There was a problem hiding this comment.
P3: For fractional durations, the documented $0.008 maximum is incorrect. creditCostForDuration can add almost one full credit, so document the bound as less than $0.01 (or derive it from an explicitly enforced duration precision).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/music/creditCostForDuration.ts, line 22:
<comment>For fractional durations, the documented `$0.008` maximum is incorrect. `creditCostForDuration` can add almost one full credit, so document the bound as less than `$0.01` (or derive it from an explicitly enforced duration precision).</comment>
<file context>
@@ -1,27 +1,36 @@
- * @returns Whole credits to gate on and, once the song lands, to deduct.
+ * Rounds up because the ledger is integer cents: one credit buys exactly five
+ * seconds at fal's rate, so durations that are not multiples of five round to
+ * the next credit. The overcharge is at most $0.008 and is an artifact of the
+ * unit, not a margin — chat#2000 proposes micro-dollar credits, which would
+ * remove it. Rounding up rather than down keeps us from paying fal more than
</file context>
| *thenextcredit.Theoverchargeisatmost$0.008andisanartifactofthe | |
| *thenextcredit.Theoverchargeislessthan$0.01andisanartifactofthe |
Two changes, both from the chat#1999 pricing decision. Rate: 0.5 -> 0.2 credits per second, and the 15-credit floor is gone. fal charges $0.002 per second and a credit is $0.01, so 0.2/s is fal's rate exactly. A 60s song drops from 30 credits to 12. The floor made a 10s song $0.15 against $0.02 of cost, 7.5x, which cannot coexist with pass-through pricing. Basis: charge the actual output length, not the requested one. The model routinely stops short — our first songs averaged 39.9s against a 60s default — so billing the request would have earned ~1.5x while calling itself pass-through. Capped at the quoted amount, because the model is not hard-capped either: a 60s request came back at 60.07s, and billing that overrun would exceed what the caller was quoted. The gate still runs on the requested duration, so a caller cannot start a 300s generation on a balance that only covers 60s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017fSvwazBitPfsTQvVqpi8q
e8df5cc to
1450f54ComparePreview verification — api#853Rebased onto The decisive runRequested 60 seconds. fal produced 15.6. That gap is the whole point of this PR, and it landed on its own.
4 credits were deducted. Confirmed twice, independently:
So all three behaviours are demonstrated by one generation: the rate is fal's ( For contrast, the two generations from api#854 testing an hour earlier show 15 credits each in the same table: 20-second songs pinned at the old floor. The same songs cost 4 under this PR. Documented vs actual
The blocker is now quantified, not theoreticalThis run makes the open question concrete and expensive. We charged the customer $0.04. If fal bills on output seconds, fal charged us $0.0312 and the pass-through is correct. If fal bills on the requested duration, fal charged us $0.12 and we just sold a song at a third of cost — losing $0.08 on a single generation, and more the further the model stops short. Our songs stop short routinely: 60→15.6 here, 60→25.9 earlier, averaging roughly two thirds of the request. I still cannot settle it from the API.
Everything else here is verified and correct. This is the only thing standing between it and merge. |
sweetmantech
commented
Aug 24, 2026
Blocker resolved — fal bills on actual output secondsThe new key reaches fal's usage API. The 22:00 UTC bucket contains exactly the two generations from this session's testing: {
"endpoint_id": "minimax/music-3",
"unit": "seconds",
"quantity": 35.62,
"unit_price": 0.002,
"cost_total": 0.07124
}
fal billed 35.62 seconds. That is the sum of what the model produced, to the hundredth. Had billing been on the requested duration it would read 80. This is arithmetic, not inference — and What it means for this PR
The remaining 0.88 cents is the artifact called out in the PR description: one credit buys five seconds, so a 15.6-second song rounds up. chat#2000's micro-dollar ledger removes it. Rounding up is the right direction — under-rounding would have put us below fal's bill. Had this shipped charging the requested duration instead, we would have taken $0.12 for a song that cost $0.03 — a 3.8x markup wearing pass-through's label, which is exactly what the "charge what fal charges us" rule existed to prevent. Merge blocker clearedThe verification above stands at 9/9, the rebase onto api#854 is clean, 87/87 tests green, and the one open question is now answered from fal's own billing data. No code change needed — the implementation was already right. Worth keeping: |
Uh oh!
There was an error while loading. Please reload this page.
Tracks recoupable/api#853. Two changes to the form's quote. Rate: 0.5 -> 0.2 credits per second and no floor, matching the API. A 60s song now quotes $0.12 rather than $0.30. A quote that disagrees with what the API charges is worse than no quote. Wording: "Costs $0.12" -> "Up to $0.12 ... You are charged for the audio actually generated." The API now bills the audio fal produced, and the model routinely stops short of the requested length, so a fixed figure would overstate what most generations are billed. An upper bound is safe in the only direction that matters: nobody is charged more than they were shown. Claude-Session: https://claude.ai/code/session_017fSvwazBitPfsTQvVqpi8q Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rows 7 of the chat#1999 matrix. This is a price cut, so no customer is worse off and no migration is needed.
Rate: fal's own
CREDITS_PER_SECOND0.5 → 0.2, andMIN_CREDIT_COSTdeleted. fal charges $0.002/s and a credit is $0.01, so 0.2 credits/s is fal's rate.The floor had to go: $0.15 for a 10-second song was 7.5x cost and flatly contradicted the policy. We absorb the workflow run, the storage write and the egress instead — deliberate, and noted on the issue.
Basis: what fal actually produced
We charged the requested duration. The model routinely stops short — across our first songs, output averaged 39.9s against a 60s default — so charging the request would have quietly earned ~1.5x while calling itself pass-through.
The deduction now prices
result.durationSeconds. The plumbing was already right: credits are deducted afterfetchMusicResultStep, so the real length was already in scope at that line.Three cases the tests pin down:
Tests
10 assertions across two files, TDD with both suites confirmed RED first. 76/76 green across
lib/musicandapp/workflows;tscandeslintclean.Before merging
Confirm on the fal invoice whether billing is on output or requested seconds. The rule is "charge what fal charges us", and this PR implements the output reading — which is the usual convention for audio models and the natural reading of "per second". If fal in fact bills the requested duration, we lose money on every early stop, which is the one outcome this is not meant to produce. I could not settle it from the API:
metricsreports onlyinference_time(41.2s for a 25.9s song, so not the billing unit) and fal's billing endpoints 404 for our key.Rounding
The ledger is integer cents, so one credit buys exactly five seconds and other durations round up by at most $0.008. That is a unit artifact rather than margin; chat#2000 proposes micro-dollar credits, which removes it. Rounding up keeps us from paying fal more than we charged.
Related
The chat form still quotes the old price — next row, separate PR.
🤖 Generated with Claude Code
https://claude.ai/code/session_017fSvwazBitPfsTQvVqpi8q
Summary by cubic
Pass-through pricing for music generation, billed on the audio fal actually produced. Previously we charged the requested duration at 0.5 credits/s with a 15-credit floor; now we charge the actual output at 0.2 credits/s with no floor, capped at the quoted amount.
creditsForCompletedGenerationinmusicGenerationWorkflow; usesresult.durationSeconds, falls back to the request when missing/<=0, and never exceeds the preflight quote (params.creditsToCharge). Gate and quote still use the requested duration.creditCostForDurationto 0.2 credits/s; remove the floor; round up to whole credits (integer-cent ledger); minimum of 1 credit for any non‑zero duration.Written for commit 1450f54. Summary will update on new commits.