Skip to content

Track xAI TTS costs from API response usage data#362

Open
gianpaj wants to merge 15 commits into
mainfrom
claude/track-api-costs-kU1I7
Open

Track xAI TTS costs from API response usage data#362
gianpaj wants to merge 15 commits into
mainfrom
claude/track-api-costs-kU1I7

Conversation

@gianpaj

@gianpaj gianpaj commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

Updated the xAI TTS integration to capture and track actual cost data from xAI's API response instead of relying on estimated pricing. The xAI API now returns cost_in_usd_ticks in the response, which is converted to USD and stored for accurate billing and usage tracking.

Changes

  • Modified generateXaiTts() to parse JSON responses from xAI API and extract cost_in_usd_ticks from the usage object
  • Updated both /api/v1/speech and /api/generate-voice routes to use actual xAI costs when available, falling back to estimated pricing for other providers
  • Store costInUsdTicks in usage metadata and calculate precise dollarAmount (1 tick = $0.000_000_001) for Grok voices
  • Updated test mocks to return JSON responses with cost data instead of raw audio buffers

How to test

  1. Run the test suites: pnpm test -- v1-speech.test.ts generate-voice.test.ts
  2. Verify that Grok TTS requests capture the cost_in_usd_ticks from xAI responses
  3. Confirm that saveAudioFileAdmin and insertUsageEvent are called with the correct cost data in both test files

Scope

  • Backend
  • Tests

Checklist

  • I self-reviewed this PR
  • I added or updated tests where needed
  • I updated docs/comments where needed

Notes for reviewers

The changes maintain backward compatibility by only using xAI's reported costs for Grok voices when available. Other TTS providers continue to use the estimated pricing table. The conversion from ticks to USD uses toFixed(6) for precision in the v1 API route to match typical currency formatting.

https://claude.ai/code/session_014T6dJTtWEyjF7Nok7K3t2u

Parse the usage.cost_in_usd_ticks field from xAI JSON TTS responses and
store the actual dollar cost (1 tick = $1e-9) in saveAudioFile usage and
insertUsageEvent. The v1/speech route now uses the exact xAI-reported cost
instead of the estimated pricing-table amount for Grok voices.

Backward-compatible: binary audio responses (no JSON body) continue to work
without cost info; cost is only extracted when the response Content-Type is
application/json.

https://claude.ai/code/session_014T6dJTtWEyjF7Nok7K3t2u
Copilot AI review requested due to automatic review settings May 4, 2026 09:33
@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sexyvoice Ready Ready Preview, Comment Jun 1, 2026 10:22am
sexyvoice-docs Ready Ready Preview, Comment Jun 1, 2026 10:22am

Request Review

@claude

claude Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @gianpaj's task in 2m 47s —— View job


Code Review

  • Read PR diff and changed files
  • Review apps/web/lib/tts/xai.ts changes
  • Review apps/web/app/api/v1/speech/route.ts changes
  • Review apps/web/app/api/generate-voice/route.ts changes
  • Review test changes
  • Post review feedback (5 inline comments)

Summary

The PR cleanly threads cost_in_usd_ticks from the xAI response through both routes and test mocks. Logic is straightforward and the conditional spreads guard against missing cost data. A few things worth addressing before merging:

Higher priority

  1. Precision drift between the two routesv1/speech/route.ts:490-498 rounds the dollar amount via Number.parseFloat((ticks / 1_000_000_000).toFixed(6)) (matching calculateExternalApiDollarAmount in lib/api/pricing.ts:82), but generate-voice/route.ts:553-555 stores the raw division. The same xAI response will be persisted with two different dollarAmount values depending on which endpoint was used. Suggest a shared usdTicksToDollarAmount(ticks) helper used by both routes and the tests. (See inline on generate-voice/route.ts:551-555.)
  2. Asymmetric dollarAmount in generate-voice — only set for Grok, while v1/speech always sets it (Grok exact + calculateExternalApiDollarAmount fallback for others). If uniform cost tracking is the goal, the user-facing route should fall back too; otherwise call out that Replicate/Gemini cost tracking is intentionally out of scope. (Inline on generate-voice/route.ts:599.)

Lower priority
3. JSON branch is now the primary success path in lib/tts/xai.ts, but the content-type check is exact-match-ish and there is no validation that the decoded base64 buffer is non-empty — a malformed JSON envelope would silently upload a JSON blob as audio. (Inline on lib/tts/xai.ts:152-169.)
4. Test coverage gap in tests/v1-speech.test.ts — only one Grok test exercises the JSON-with-cost path; the others still mock raw arrayBuffer, which is the legacy branch. Worth migrating them so a regression in JSON parsing is caught by more than a single test. (Inline on tests/v1-speech.test.ts:274-344.)
5. Magic number 1_000_000_000 repeated in three places; collapsing into the helper above also fixes this. (Inline on v1/speech/route.ts:488-498.)

No security issues spotted: cost data flows from a trusted upstream, is parsed defensively, and never round-trips back to the user. Credit deduction still uses the local estimator, so a manipulated xAI response can't drain credits.
• Branch: claude/track-api-costs-kU1I7

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements precise cost tracking for xAI (Grok) text-to-speech by extracting cost_in_usd_ticks from API responses and calculating the dollar amount across the generate-voice and V1 speech routes. The generateXaiTts utility was updated to handle JSON responses, and tests were added to verify the new tracking logic. Feedback suggests removing a toFixed(6) call in the V1 route to prevent precision loss and maintain consistency with the internal route. Additionally, the V1 route's usage metadata should be updated to include the codec for parity between endpoints.

Comment thread apps/web/app/api/v1/speech/route.ts Outdated
Comment thread apps/web/app/api/v1/speech/route.ts Outdated
Comment thread apps/web/app/api/generate-voice/route.ts Outdated
Comment thread apps/web/app/api/generate-voice/route.ts Outdated
- Remove toFixed(6) to preserve full nanotick precision for dollar amounts
- Add codec to Grok voice usage event metadata for parity with generate-voice route
- Hoist grokCodec variable so it's accessible at the insertUsageEvent call site
- Update test assertion to match both changes

https://claude.ai/code/session_014T6dJTtWEyjF7Nok7K3t2u
Comment thread apps/web/lib/tts/xai.ts Outdated
Comment thread apps/web/app/api/v1/speech/route.ts

Copilot AI 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.

Pull request overview

This PR updates the xAI (Grok) TTS integration to ingest the usage.cost_in_usd_ticks value returned by xAI and use it to record more accurate cost/usage data (with backward compatibility when the cost field is absent).

Changes:

  • Updated generateXaiTts() to handle JSON responses containing base64 audio plus usage.cost_in_usd_ticks.
  • Updated /api/v1/speech and /api/generate-voice to propagate costInUsdTicks into usage metadata and compute dollarAmount from ticks.
  • Updated route tests to mock xAI JSON responses and assert that cost metadata is persisted and usage events include dollar amounts.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/web/lib/tts/xai.ts Adds JSON-response parsing for audio + usage cost ticks and returns costInUsdTicks to callers.
apps/web/app/api/v1/speech/route.ts Captures Grok tick costs and uses them to compute dollarAmount (fallback to estimated pricing otherwise).
apps/web/app/api/generate-voice/route.ts Captures Grok tick costs and stores them into audio file usage + usage event metadata.
apps/web/tests/v1-speech.test.ts Updates xAI mock to return JSON with cost ticks and asserts persistence/usage event fields.
apps/web/tests/generate-voice.test.ts Updates xAI mock to return JSON with cost ticks and asserts persistence/usage event fields.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/web/app/api/generate-voice/route.ts Outdated
Comment thread apps/web/app/api/generate-voice/route.ts
Comment thread apps/web/tests/generate-voice.test.ts Outdated
Comment thread apps/web/lib/tts/xai.ts
…sing

- Add usdTicksToDollarAmount() to lib/tts/xai.ts so both routes use the
  same tick-to-dollar conversion and no inline constants can drift
- Make JSON content-type check case-insensitive (.toLowerCase().includes('json'))
  to handle variants like 'application/json; charset=utf-8'
- Validate decoded base64 audio buffer is non-empty before returning
- Fix generate-voice route to only spread dollarAmount when defined
  (avoid explicit null; let insertUsageEvent handle the default)
- Update both tests to import and use usdTicksToDollarAmount

https://claude.ai/code/session_014T6dJTtWEyjF7Nok7K3t2u
…icks

- usdTicksToDollarAmount now rounds to 6 decimal places to match the
  numeric(12,6) DB column, preventing silent Postgres rounding that would
  produce different values than what the app logged / computed in-memory
- Add validateTicks() to coerce cost_in_usd_ticks to undefined when the
  value is not a finite non-negative number, preventing NaN propagation
  if xAI ever returns a string, null, or unexpected type for that field

https://claude.ai/code/session_014T6dJTtWEyjF7Nok7K3t2u

Copilot AI 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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings May 11, 2026 12:22

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 8 comments.

? usdTicksToDollarAmount(grokCostInUsdTicks)
: calculateExternalApiDollarAmount({
sourceType: 'api_tts',
provider,
Comment on lines +575 to +578
const grokDollarAmount =
grokCostInUsdTicks === undefined
? undefined
: usdTicksToDollarAmount(grokCostInUsdTicks);
Comment thread apps/web/lib/utils.ts
Comment on lines +39 to +42
const GROK_CHAR_BUCKET = 50;
const GROK_CREDITS_PER_BUCKET = 50;

// our cost
Comment thread apps/web/lib/utils.ts Outdated
Comment on lines +128 to +131
// $15.00 / 1M characters
// ~4 credits per character (~4,000 credits per minute of audio)
// Gross margin:
// 99.5%

- **Gpro voices**: ~1 credit per token (based on text and audio length)
- **Grok voices**: 4 credits per 100 characters
- **Grok voices**: 100 credits per 100 characters
Comment thread apps/web/tests/api-v1-meta.test.ts Outdated
Comment on lines +21 to +24
expect(response.status).toBe(200);
expect(json.data).toHaveLength(3);
expect(json.data[0].id).toBe('gpro');
expect(json.data[2].id).toBe('xai');
Comment thread apps/web/tests/generate-voice.test.ts Outdated
const xaiResponseBuffer = new Uint8Array([10, 20, 30, 40]).buffer;
const xaiAudioBytes = new Uint8Array([10, 20, 30, 40]);
const xaiAudioBase64 = Buffer.from(xaiAudioBytes).toString('base64');
const xaiCostInUsdTicks = 1950;
expect(json.url).toContain('files.sexyvoice.ai');
expect(json.url).toContain('.mp3');

const expectedDollarAmount = usdTicksToDollarAmount(xaiCostInUsdTicks);
Replace 1950/1650 tick values (which round away to 0.000002, causing
2.6% precision loss) with 15_000_000 (= $0.000015 exactly at 6dp,
matching the fallback pricing rate). Also update the v1/speech primary
Grok test to use a JSON mock response and add assertions on
saveAudioFileAdmin and insertUsageEvent for costInUsdTicks and
dollarAmount, so the cost-tracking paths are actually exercised.

https://claude.ai/code/session_014T6dJTtWEyjF7Nok7K3t2u
…ET changed to 50

GROK_CHAR_BUCKET/GROK_CREDITS_PER_BUCKET were halved (100→50) upstream,
making estimateGrokCredits("Hello world") return 50 instead of 100.

https://claude.ai/code/session_014T6dJTtWEyjF7Nok7K3t2u

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.

? usdTicksToDollarAmount(grokCostInUsdTicks)
: calculateExternalApiDollarAmount({
sourceType: 'api_tts',
provider,
Comment on lines +271 to +275
sourceId: 'test-audio-file-id',
model: 'xai',
dollarAmount: expectedDollarAmount,
creditsUsed: 50,
metadata: expect.objectContaining({
Comment thread apps/web/lib/utils.ts Outdated
// $4.20 / 1M characters
// ~1 credit per character (~1,000 credits per minute of audio)
// $15.00 / 1M characters
// ~4 credits per character (~4,000 credits per minute of audio)
Comment on lines +575 to +579
const grokDollarAmount =
grokCostInUsdTicks === undefined
? undefined
: usdTicksToDollarAmount(grokCostInUsdTicks);

Comment on lines 118 to +126
response_format: z
.enum(['wav', 'mp3'])
.optional()
.describe('Audio format. Default depends on model'),
.describe('Audio format. `xai` supports both `mp3` and `wav`.'),
style: z
.string()
.optional()
.describe('Emotion/style variant (e.g., "happy", "sad", "whisper")'),
.describe(
'Ignored for `xai` requests. Use Grok speech tags in `input` instead.',
…k, fix stale comments

- v1/speech: restore missing `model` arg in calculateExternalApiDollarAmount —
  without it the key lookup falls back to ZERO_PRICE for all providers
- generate-voice: fall back to calculateGrokTtsDollarAmount(text) when xAI
  doesn't return cost_in_usd_ticks (binary audio path), so cost tracking
  has no gaps; also include dollarAmount in saveAudioFile for all Grok voices
- utils.ts: replace stale "~4 credits per char / Gross margin 99.5%" comment
  with accurate description of the 50-char bucket billing logic
- docs: update billing description to reflect 50-char buckets (was "100 per 100")
- api-v1-meta test: use set membership (toContain) instead of hard-coded
  index assertions so ordering changes don't break the test

https://claude.ai/code/session_014T6dJTtWEyjF7Nok7K3t2u

Copilot AI 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.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

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.

3 participants