Skip to content

feat: support gpt tts series model quota calculate - #2434

Merged
Calcium-Ion merged 2 commits into
mainfrom
feat/gpt-4o-mini-tts
Dec 13, 2025
Merged

feat: support gpt tts series model quota calculate#2434
Calcium-Ion merged 2 commits into
mainfrom
feat/gpt-4o-mini-tts

Conversation

@Calcium-Ion

@Calcium-Ion Calcium-Ion commented Dec 13, 2025

Copy link
Copy Markdown
Member
QQ_1765618163608

对于gpt-4o-mini-tts的价格,按以下设置倍率规则设置:

模型倍率 0.30,补全倍率 20.00,音频倍率 25.00,音频补全倍率 1.00

补全倍率计算规则:
输入价格为$0.6,输出价格为$12,$12/$0.6=20

其中流模式官方会返回token数量,因此无需使用到音频倍率,直接使用补全倍率计算
非流模式官方不返回token,因此使用本地计费,所以需要用到音频倍率

音频计算规则为:

  1. 图中给出的音频输入预估$0.015 / minute,换算为$15
  2. $15 / $0.6 = 25,得出音频输入价格,因此得出音频倍率=25
  3. 图中给出的音频输出预估$0.015 / minute,换算为$15
  4. $15 / $15 = 1,因此得出音频补全倍率=1

Summary by CodeRabbit

  • New Features

    • Added handlers for OpenAI text‑to‑speech and speech‑to‑text with streaming and non‑streaming support.
  • Improvements

    • Better streaming detection and handling for audio responses.
    • Token counting refined for audio requests (model-aware selection).
    • Quota consumption now bases audio billing on actual audio token usage rather than model name.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Dec 13, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Refactors OpenAI audio handling by extracting TTS/STT handlers, changes AudioRequest token-type selection and stream detection, and switches post-response quota routing to use actual audio token presence; also adjusts a hardcoded model ratio.

Changes

Cohort / File(s) Summary
DTO tweaks
dto/audio.go
Use strings import; AudioRequest.GetTokenCountMeta selects TokenTypeTokenizer when model contains "gpt"; AudioRequest.IsStream returns true only for StreamFormat == "sse".
Quota routing updates
relay/audio_handler.go, relay/compatible_handler.go
Post-response quota now checks CompletionTokenDetails.AudioTokens or PromptTokensDetails.AudioTokens and calls PostAudioConsumeQuota when audio tokens present; otherwise falls back to postConsumeQuota. Replaces prior model-name based gating.
Audio handlers extracted / added
relay/channel/openai/audio.go, relay/channel/openai/relay-openai.go
Added OpenaiTTSHandler and OpenaiSTTHandler in audio.go; removed them from relay-openai.go; replaced encoding/json unmarshal calls with common.Unmarshal in affected call sites.
Model ratio change
setting/ratio_setting/model_ratio.go
For gpt-4o-2024-05-13, return value changed to (4, false) instead of (3, true).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Relay as Relay (gin)
    participant OpenAI as OpenAI Upstream
    participant Quota as Quota Service
    participant UsageParser as Usage parsing/token estimation

    Client->>Relay: Send audio TTS/STT request
    Relay->>OpenAI: Forward request (streaming or non-stream)
    alt streaming chunks
        OpenAI-->>Relay: Stream chunks (may contain "usage" JSON chunks)
        Relay->>UsageParser: Parse streaming usage updates
        UsageParser-->>Relay: Updated usage (prompt/completion/audio tokens)
        Relay->>Client: Forward audio chunks
    else non-streaming response
        OpenAI-->>Relay: Full response body
        Relay->>Client: Write body to client
        Relay->>UsageParser: Extract/estimate usage from body (duration, size, upstream usage)
    end
    Relay->>Quota: If usage.AudioTokens > 0 -> PostAudioConsumeQuota else -> postConsumeQuota
    Quota-->>Relay: Quota response/ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review streaming parsing and incremental usage updates in relay/channel/openai/audio.go.
  • Validate audio duration heuristics (PCM fallback) and token estimation edge cases.
  • Confirm quota-routing logic in relay/audio_handler.go and relay/compatible_handler.go behaves correctly for mixed token types.
  • Check GetTokenCountMeta model string check and IsStream change for downstream impacts.

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • seefs001

Poem

🐰 I hopped through streams and token trails,
Moved TTS and STT down new rails,
I counted hops by audio tone,
Routed quotas by tokens shown,
A tiny hop, a quieter code — hooray, carrots for all! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support gpt tts series model quota calculate' directly describes the main objective of the PR: adding quota calculation support for the gpt-4o-mini-tts model series, which is evidenced by the changes across multiple files implementing token counting and quota handling for TTS/STT operations.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/gpt-4o-mini-tts

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e36e2e1 and 7cae4a6.

📒 Files selected for processing (1)
  • relay/channel/openai/audio.go (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-21T06:31:11.073Z
Learnt from: jiajunly
Repo: QuantumNous/new-api PR: 1629
File: relay/channel/openai/relay-openai.go:170-174
Timestamp: 2025-08-21T06:31:11.073Z
Learning: In relay/channel/openai/relay-openai.go, the streaming logic for the AddThinkFirst feature is designed so that only the first chunk of a stream gets the "<think>\n" prefix. The final flush in the streaming handler intentionally uses addThink=false because the last chunk should never receive the prefix, even in single-chunk streams where the prefix would have been applied during normal processing.

Applied to files:

  • relay/channel/openai/audio.go
📚 Learning: 2025-06-21T03:37:41.726Z
Learnt from: 9Ninety
Repo: QuantumNous/new-api PR: 1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.

Applied to files:

  • relay/channel/openai/audio.go
🔇 Additional comments (3)
relay/channel/openai/audio.go (3)

80-91: LGTM on PCM duration calculation.

The PCM format handling correctly accounts for OpenAI TTS's output parameters (24kHz sample rate, 16-bit depth, mono channel). The fallback to GetAudioDuration for other formats is appropriate.


114-145: LGTM on STT handler.

The handler correctly:

  • Normalizes token field names from OpenAI's convention to the internal convention
  • Provides a sensible fallback when usage data is unavailable
  • Handles errors gracefully

93-107: Verify that detail structs are not nil pointers.

Lines 93, 101, and 106 access usage.PromptTokensDetails.TextTokens and usage.CompletionTokenDetails.AudioTokens on a zero-initialized &dto.Usage{}. If these detail fields are pointer types, this will cause a nil pointer panic.

#!/bin/bash
# Description: Check if PromptTokensDetails and CompletionTokenDetails are pointer or value types in dto.Usage
# Expected: These should be embedded structs (not pointers) for the code to be safe

ast-grep --pattern 'type Usage struct {
  $$$
}'

# Also check for any pointer fields
rg -n "PromptTokensDetails|CompletionTokenDetails" --type go -A2 -B2

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.

@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: 3

🧹 Nitpick comments (2)
dto/audio.go (1)

28-30: Consider case-insensitive model name matching.

The check strings.Contains(r.Model, "gpt") is case-sensitive. If a model name ever arrives in a different case (e.g., "GPT-4o-mini-tts"), the tokenizer type won't be applied correctly. Consider using strings.Contains(strings.ToLower(r.Model), "gpt") for robustness.

-	if strings.Contains(r.Model, "gpt") {
+	if strings.Contains(strings.ToLower(r.Model), "gpt") {
 		meta.TokenType = types.TokenTypeTokenizer
 	}
relay/channel/openai/audio.go (1)

105-105: Simplify redundant rounding.

math.Round(math.Ceil(duration)) is redundant since math.Ceil already returns a whole number. Consider simplifying.

-		completionTokens := int(math.Round(math.Ceil(duration) / 60.0 * 1000))
+		completionTokens := int(math.Ceil(duration / 60.0 * 1000))
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b602843 and e36e2e1.

📒 Files selected for processing (6)
  • dto/audio.go (2 hunks)
  • relay/audio_handler.go (1 hunks)
  • relay/channel/openai/audio.go (1 hunks)
  • relay/channel/openai/relay-openai.go (2 hunks)
  • relay/compatible_handler.go (1 hunks)
  • setting/ratio_setting/model_ratio.go (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-06-21T03:37:41.726Z
Learnt from: 9Ninety
Repo: QuantumNous/new-api PR: 1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.

Applied to files:

  • relay/compatible_handler.go
📚 Learning: 2025-08-21T06:31:11.073Z
Learnt from: jiajunly
Repo: QuantumNous/new-api PR: 1629
File: relay/channel/openai/relay-openai.go:170-174
Timestamp: 2025-08-21T06:31:11.073Z
Learning: In relay/channel/openai/relay-openai.go, the streaming logic for the AddThinkFirst feature is designed so that only the first chunk of a stream gets the "<think>\n" prefix. The final flush in the streaming handler intentionally uses addThink=false because the last chunk should never receive the prefix, even in single-chunk streams where the prefix would have been applied during normal processing.

Applied to files:

  • relay/channel/openai/audio.go
📚 Learning: 2025-08-05T17:14:17.246Z
Learnt from: neotf
Repo: QuantumNous/new-api PR: 1511
File: setting/ratio_setting/model_ratio.go:118-123
Timestamp: 2025-08-05T17:14:17.246Z
Learning: Claude models handle "-thinking" variants differently from Gemini models. For Claude models, only the base model (without "-thinking") gets an entry in defaultModelRatio map. The "-thinking" variants rely on the Claude relay handler stripping the suffix using strings.TrimSuffix(textRequest.Model, "-thinking") before looking up the ratio, so they automatically use the base model's ratio.

Applied to files:

  • setting/ratio_setting/model_ratio.go
🧬 Code graph analysis (4)
relay/audio_handler.go (2)
dto/openai_response.go (1)
  • Usage (222-240)
service/quota.go (1)
  • PostAudioConsumeQuota (377-478)
relay/compatible_handler.go (1)
dto/openai_response.go (1)
  • Usage (222-240)
dto/audio.go (2)
model/model_meta.go (1)
  • Model (23-44)
types/request_meta.go (2)
  • TokenType (12-12)
  • TokenTypeTokenizer (16-16)
relay/channel/openai/relay-openai.go (1)
common/json.go (1)
  • Unmarshal (9-11)
🔇 Additional comments (7)
setting/ratio_setting/model_ratio.go (1)

534-540: LGTM - Allows completion ratio override for gpt-4o models.

The change from true to false for the gpt-4o fallback allows the CompletionRatio map to override the hardcoded value of 4, providing more flexibility for configuration. The special case for gpt-4o-2024-05-13 (ratio 3) is preserved.

dto/audio.go (1)

34-36: LGTM - Proper SSE stream detection.

The IsStream method now correctly returns true only when StreamFormat == "sse", aligning with the expected streaming behavior for audio requests.

relay/compatible_handler.go (1)

184-188: LGTM - Data-driven audio quota detection is a good improvement.

Replacing model-name based gating (strings.HasPrefix(name, "gpt-4o-audio")) with actual audio token presence detection is more robust and will correctly handle any model that produces audio output, including new GPT TTS models.

relay/channel/openai/relay-openai.go (2)

153-154: LGTM - Consistent use of common.Unmarshal.

Using common.Unmarshal instead of direct json.Unmarshal aligns with the codebase pattern and allows for centralized JSON handling if needed in the future.


627-628: LGTM - Consistent unmarshalling pattern.

Same consistency improvement as other locations in this file.

relay/channel/openai/audio.go (2)

80-91: LGTM - PCM duration calculation.

The PCM format handling with hardcoded OpenAI TTS parameters (24000 Hz, 16-bit, mono) is correct. The fallback to GetAudioDuration for other formats is appropriate.


114-145: LGTM - STT handler with proper usage fallback.

The STT handler correctly parses usage from the response and falls back to estimated tokens when not available. The normalization of InputTokens/OutputTokens to PromptTokens/CompletionTokens handles different API response formats.

Comment thread relay/audio_handler.go
Comment on lines +70 to +74
if usage.(*dto.Usage).CompletionTokenDetails.AudioTokens > 0 || usage.(*dto.Usage).PromptTokensDetails.AudioTokens > 0 {
service.PostAudioConsumeQuota(c, info, usage.(*dto.Usage), "")
} else {
postConsumeQuota(c, info, usage.(*dto.Usage), "")
}

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

Add nil check before accessing usage fields to prevent potential panic.

If adaptor.DoResponse returns nil for usage without an error (edge case), the type assertion and field access will cause a panic. Consider adding a nil check.

+	if usage == nil {
+		usage = &dto.Usage{
+			PromptTokens: info.GetEstimatePromptTokens(),
+			TotalTokens:  info.GetEstimatePromptTokens(),
+		}
+	}
 	if usage.(*dto.Usage).CompletionTokenDetails.AudioTokens > 0 || usage.(*dto.Usage).PromptTokensDetails.AudioTokens > 0 {
 		service.PostAudioConsumeQuota(c, info, usage.(*dto.Usage), "")
 	} else {
 		postConsumeQuota(c, info, usage.(*dto.Usage), "")
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if usage.(*dto.Usage).CompletionTokenDetails.AudioTokens > 0 || usage.(*dto.Usage).PromptTokensDetails.AudioTokens > 0 {
service.PostAudioConsumeQuota(c, info, usage.(*dto.Usage), "")
} else {
postConsumeQuota(c, info, usage.(*dto.Usage), "")
}
if usage == nil {
usage = &dto.Usage{
PromptTokens: info.GetEstimatePromptTokens(),
TotalTokens: info.GetEstimatePromptTokens(),
}
}
if usage.(*dto.Usage).CompletionTokenDetails.AudioTokens > 0 || usage.(*dto.Usage).PromptTokensDetails.AudioTokens > 0 {
service.PostAudioConsumeQuota(c, info, usage.(*dto.Usage), "")
} else {
postConsumeQuota(c, info, usage.(*dto.Usage), "")
}
🤖 Prompt for AI Agents
In relay/audio_handler.go around lines 70 to 74, the code assumes usage is
non-nil and does a type assertion and field access directly which can panic if
adaptor.DoResponse returns nil; add a nil check after obtaining usage (and
before the type assertion/cast) and handle the nil case (e.g., call
postConsumeQuota with a nil/empty usage or return/skip as appropriate), or
perform a safe type assertion that checks for nil before accessing
CompletionTokenDetails/PromptTokensDetails to avoid dereferencing a nil pointer.

Comment thread relay/channel/openai/audio.go
Comment thread relay/channel/openai/audio.go
@Calcium-Ion
Calcium-Ion merged commit 7a307e2 into main Dec 13, 2025
1 check passed
Ridter pushed a commit to Ridter/new-api that referenced this pull request Dec 13, 2025
…-tts

feat: support gpt tts series model quota calculate
@Calcium-Ion
Calcium-Ion deleted the feat/gpt-4o-mini-tts branch January 29, 2026 17:10
ennnnny pushed a commit to ennnnny/new-api that referenced this pull request Mar 17, 2026
…-tts

feat: support gpt tts series model quota calculate
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