Skip to content

feat(voice): price gpt-realtime token usage and add realtime session constants - #2391

Merged
2witstudios merged 1 commit into
pu/gpt-realtimefrom
pu/rt-pricing
Aug 10, 2026
Merged

feat(voice): price gpt-realtime token usage and add realtime session constants#2391
2witstudios merged 1 commit into
pu/gpt-realtimefrom
pu/rt-pricing

Conversation

@2witstudios

Copy link
Copy Markdown
Owner

Third leaf of the audio-native voice epic. Realtime bills tokens per modality, not audio-seconds and characters, so it needs its own table beside the existing whisper/tts rates.

Rates measured against the live API in the W0 spike, not guessed — including the usage shape, which is not the Chat Completions shape (input_tokens/output_tokens, with per-modality detail nested one level down).

Three asymmetries built against rather than around:

  • Caching is input-only — there is no cached figure on the output side, and the rate type refuses to model one.
  • cached_tokens is a SUBSET of input_tokens, so cached is billed at the cached rate and only the remainder at the full rate. Double-counting here would silently overcharge every call.
  • Audio output dominates cost (86 audio vs 30 text tokens for one short spoken sentence), so metering is per-modality — a single total would hide the actual cost driver.

Also adds realtime session constants (hold estimate, max duration, idle timeout, concurrency cap) — the account's measured ceiling is 40,000 tokens/min, so unbounded concurrent calls would hit it.

Verification: 52 tests pass; rates asserted against published figures. Mutation-checked the double-count guard — replacing (gross − cached) with gross turns 6 tests red.

Additive only: the existing whisper/tts rates and behavior are untouched, because open PR #2173 (Read Aloud) depends on the tts path.

…constants

Voice bills deterministically today by audio-second (whisper) and input-character
(tts). The audio-native realtime path bills TOKENS, per modality, in a shape
different enough to need its own table. Additive only: the whisper/tts rates and
behaviour are untouched, since on-demand TTS (Read Aloud) is not being retired.

voice-pricing.ts
- REALTIME_RATES: published gpt-realtime rates as USD per token, every rate
  env-overridable via the file's existing envFloat, defaults pinned by tests.
- calculateRealtimeCostDollars(model, usage): pre-markup provider cost for one
  response.done frame; the credit pipeline applies MARKUP_BPS as it does for
  every other call. A call's total is the sum across its responses, since usage
  is reported per RESPONSE, not per call.

Three wire asymmetries are modelled rather than smoothed over:
- Caching is input-only, so the output rate type has no cached member to fill in
  wrongly, and no image member either.
- cached_tokens is a SUBSET of input_tokens: the cached slice bills at the cache
  rate and only the REMAINDER at the full input rate. Each modality's cached
  count is clamped to its own gross count, so a malformed payload cannot drive
  the fresh remainder negative and subtract money from the bill.
- Modality is the cost driver (one short spoken sentence measured 30 text + 86
  audio output tokens, audio being ~2.7x the text rate), so each side is priced
  per modality rather than blended.

Unknown model, absent usage, or missing/negative/NaN quantities bill 0 — never a
negative or NaN charge. A usage object carrying only totals also bills 0: an
unattributed token cannot be priced (audio input is 8x text input) and guessing a
modality would over-charge, so the metering layer is left to notice that shape.

credit-pricing.ts — realtime session constants beside the existing voice ones:
- REALTIME_SESSION_HOLD_ESTIMATE_CENTS (10c). Realtime settles CONTINUOUSLY as
  usage arrives per response, so the hold need only cover the window between
  settles rather than the whole call.
- REALTIME_MAX_SESSION_SECONDS (600). Derived, not picked: it must stay inside
  CREDIT_HOLD_TTL_SECONDS (900) or the reconcile cron reclaims a live call's own
  reservation mid-session. A test asserts a >=300s margin.
- REALTIME_IDLE_TIMEOUT_SECONDS (120). Reaps the abandoned call, which otherwise
  holds a slot and streams ambient audio into a per-token-billed model.
- REALTIME_MAX_INFLIGHT (2 per user). Voice is physically exclusive, so 1 is the
  semantically correct cap; 2 keeps a not-yet-reaped zombie session from locking
  a user out of reconnecting.
- REALTIME_MAX_GLOBAL_SESSIONS (8). The per-user cap structurally cannot enforce
  the binding constraint: the OpenAI account is limited to 40,000 tokens/MINUTE
  across every session on the key, which a talking session approaches at roughly
  4k tokens/min — putting the ceiling near 10.

Pure: no route wiring, that is the metering leaf's job.

Verified: voice-pricing.ts at 100% statement/branch/function/line coverage, with
7 mutations (double-count guard, cached clamp, a published rate, the negative
quantity guard, the pro-rata share cap, and both session constants) each
confirmed to turn the suite red. Root typecheck 17/17 and lint 15/15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JCyj6kA4A7bwmmpwDhHrer
@2witstudios
2witstudios merged commit 598f741 into pu/gpt-realtime Aug 10, 2026
1 check was pending
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ca02b6b-90d2-4f93-b773-e69ed562fc70

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

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

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