Skip to content

feat: use audio token usage if return - #1721

Merged
creamlike1024 merged 1 commit into
QuantumNous:alphafrom
feitianbubu:pr/opt-audio-usage
Sep 2, 2025
Merged

feat: use audio token usage if return#1721
creamlike1024 merged 1 commit into
QuantumNous:alphafrom
feitianbubu:pr/opt-audio-usage

Conversation

@feitianbubu

@feitianbubu feitianbubu commented Sep 2, 2025

Copy link
Copy Markdown
Member

openai旧模型whisper-1由于没有返回usage,所以使用本地声音长度预估, 比较麻烦还得安装ffmpeg
新的模型gpt-4o-transcribe已经可以返回usage, 所以改用返回的usage会更精确
image

使用日志也会记录更准确
image

Summary by CodeRabbit

  • Bug Fixes
    • Corrected token accounting for speech-to-text requests by prioritizing upstream usage data, ensuring accurate totals and consistent prompt/completion breakdown.
    • Preserves reliability by automatically falling back to local counting when upstream usage is unavailable, with no change to streamed responses.

@coderabbitai

coderabbitai Bot commented Sep 2, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

The STT handler in relay/channel/openai/relay-openai.go now first attempts to parse token usage from the upstream JSON response. If present, it normalizes and returns those values. If not, it falls back to counting tokens from the audio. Streaming behavior to the client remains unchanged.

Changes

Cohort / File(s) Summary
OpenAI relay STT usage handling
relay/channel/openai/relay-openai.go
Added JSON parsing for upstream usage in OpenaiSTTHandler, introduced a responseData struct, prioritized upstream token usage (with normalization) over local audio-based token counting, and retained streaming of the response body. Removed direct duration-based counting path in favor of upstream-usage-first flow with fallback to countAudioTokens.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Relay as Relay (OpenaiSTTHandler)
  participant Upstream as OpenAI Upstream

  Client->>Relay: STT request (audio)
  Relay->>Upstream: Forward request (stream response)
  Upstream-->>Relay: Streamed response body

  rect rgba(220,240,255,0.4)
    note over Relay: Read upstream body chunk(s)
    Relay->>Relay: Try JSON unmarshal { usage }
    alt usage present and tokens > 0
      Relay->>Relay: Normalize Prompt/Completion from Input/Output if needed
      note over Relay: Use upstream usage (early)
    else usage missing/zero
      Relay->>Relay: Fallback: countAudioTokens(audio)
      alt count ok
        Relay->>Relay: Use counted tokens
      else error
        Relay->>Relay: Preserve prior error handling
      end
    end
  end

  Relay-->>Client: Stream response body (unchanged)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I twitch my ears at JSON’s tune,
Upstream whispers tokens, sweet as moon.
If silence falls, I count my crumbs,
From audio trails where rhythm hums.
Hop, hop—normalize, then stream we go,
A tidy warren of usage flow. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

🧹 Nitpick comments (1)
relay/channel/openai/relay-openai.go (1)

5-5: Minor consistency: prefer common.Unmarshal to avoid mixed JSON decoders

Elsewhere this file uses common.Unmarshal; using a single decoder keeps behavior uniform (e.g., jsoniter, number handling) and lets you drop the extra import.

Apply:

-import "encoding/json"
+// no need for encoding/json if using common.Unmarshal

And:

-if err := json.Unmarshal(responseBody, &responseData); err == nil && responseData.Usage != nil {
+if err := common.Unmarshal(responseBody, &responseData); err == nil && responseData.Usage != nil {

Also applies to: 294-294

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5cbd9da and f7ae362.

📒 Files selected for processing (1)
  • relay/channel/openai/relay-openai.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
relay/channel/openai/relay-openai.go (2)
dto/openai_response.go (1)
  • Usage (217-230)
types/error.go (2)
  • NewError (187-199)
  • ErrorCodeCountTokenFailed (42-42)

Comment on lines +291 to +305
var responseData struct {
Usage *dto.Usage `json:"usage"`
}
if err := json.Unmarshal(responseBody, &responseData); err == nil && responseData.Usage != nil {
if responseData.Usage.TotalTokens > 0 {
usage := responseData.Usage
if usage.PromptTokens == 0 {
usage.PromptTokens = usage.InputTokens
}
if usage.CompletionTokens == 0 {
usage.CompletionTokens = usage.OutputTokens
}
return nil, 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.

🛠️ Refactor suggestion

Normalize upstream usage even when total_tokens is missing; also propagate token details

Many providers return only input/output tokens (and set total_tokens to 0). Your current gate requires TotalTokens > 0, which will incorrectly fall back to local estimation and under/over bill. Normalize as soon as any of Input/Output/Prompt/Completion is present, compute TotalTokens if absent, and map InputTokensDetails into PromptTokensDetails (mirrors OpenaiHandlerWithUsage).

Apply:

 var responseData struct {
   Usage *dto.Usage `json:"usage"`
 }
-if err := json.Unmarshal(responseBody, &responseData); err == nil && responseData.Usage != nil {
-    if responseData.Usage.TotalTokens > 0 {
-        usage := responseData.Usage
-        if usage.PromptTokens == 0 {
-            usage.PromptTokens = usage.InputTokens
-        }
-        if usage.CompletionTokens == 0 {
-            usage.CompletionTokens = usage.OutputTokens
-        }
-        return nil, usage
-    }
-}
+if err := json.Unmarshal(responseBody, &responseData); err == nil && responseData.Usage != nil {
+    u := responseData.Usage
+    // Fill prompt/completion from input/output if missing
+    if u.PromptTokens == 0 && u.InputTokens > 0 {
+        u.PromptTokens = u.InputTokens
+    }
+    if u.CompletionTokens == 0 && u.OutputTokens > 0 {
+        u.CompletionTokens = u.OutputTokens
+    }
+    // Compute total if missing but components exist
+    if u.TotalTokens == 0 && (u.PromptTokens > 0 || u.CompletionTokens > 0) {
+        u.TotalTokens = u.PromptTokens + u.CompletionTokens
+    }
+    // Propagate input token details to prompt details if provided
+    if u.InputTokensDetails != nil {
+        u.PromptTokensDetails.TextTokens += u.InputTokensDetails.TextTokens
+        u.PromptTokensDetails.ImageTokens += u.InputTokensDetails.ImageTokens
+        u.PromptTokensDetails.AudioTokens += u.InputTokensDetails.AudioTokens
+    }
+    if u.TotalTokens > 0 || u.PromptTokens > 0 || u.CompletionTokens > 0 {
+        return nil, u
+    }
+}
📝 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
var responseData struct {
Usage *dto.Usage `json:"usage"`
}
if err := json.Unmarshal(responseBody, &responseData); err == nil && responseData.Usage != nil {
if responseData.Usage.TotalTokens > 0 {
usage := responseData.Usage
if usage.PromptTokens == 0 {
usage.PromptTokens = usage.InputTokens
}
if usage.CompletionTokens == 0 {
usage.CompletionTokens = usage.OutputTokens
}
return nil, usage
}
}
var responseData struct {
Usage *dto.Usage `json:"usage"`
}
if err := json.Unmarshal(responseBody, &responseData); err == nil && responseData.Usage != nil {
u := responseData.Usage
// Fill prompt/completion from input/output if missing
if u.PromptTokens == 0 && u.InputTokens > 0 {
u.PromptTokens = u.InputTokens
}
if u.CompletionTokens == 0 && u.OutputTokens > 0 {
u.CompletionTokens = u.OutputTokens
}
// Compute total if missing but components exist
if u.TotalTokens == 0 && (u.PromptTokens > 0 || u.CompletionTokens > 0) {
u.TotalTokens = u.PromptTokens + u.CompletionTokens
}
// Propagate input token details to prompt details if provided
if u.InputTokensDetails != nil {
u.PromptTokensDetails.TextTokens += u.InputTokensDetails.TextTokens
u.PromptTokensDetails.ImageTokens += u.InputTokensDetails.ImageTokens
u.PromptTokensDetails.AudioTokens += u.InputTokensDetails.AudioTokens
}
// Return as long as any token data is present
if u.TotalTokens > 0 || u.PromptTokens > 0 || u.CompletionTokens > 0 {
return nil, u
}
}

Comment on lines +307 to +310
audioTokens, err := countAudioTokens(c)
if err != nil {
return types.NewError(err, types.ErrorCodeCountTokenFailed), nil
}

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

Avoid returning an error after writing the upstream body to the client

By Line 289 you’ve already flushed the upstream response. Returning a non-nil error here risks double-send/error paths and inconsistent retries. Align with the TTS handler: log and return a zero-usage fallback instead of surfacing an error.

Apply:

 audioTokens, err := countAudioTokens(c)
 if err != nil {
-    return types.NewError(err, types.ErrorCodeCountTokenFailed), nil
+    logger.LogError(c, fmt.Sprintf("count audio tokens failed: %v", err))
+    // After body is sent, do not bubble errors; return zero-usage fallback.
+    return nil, &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
audioTokens, err := countAudioTokens(c)
if err != nil {
return types.NewError(err, types.ErrorCodeCountTokenFailed), nil
}
audioTokens, err := countAudioTokens(c)
if err != nil {
logger.LogError(c, fmt.Sprintf("count audio tokens failed: %v", err))
// After body is sent, do not bubble errors; return zero-usage fallback.
return nil, &dto.Usage{}
}
🤖 Prompt for AI Agents
In relay/channel/openai/relay-openai.go around lines 307–310, do not return a
non-nil error after the upstream response has already been flushed; instead,
catch the countAudioTokens error, log the failure with context, set audioTokens
(or the usage result) to a zero-usage fallback value, and continue execution
returning nil error so we avoid double-send/retry paths (mirror the TTS handler
behavior).

@creamlike1024
creamlike1024 merged commit 1702449 into QuantumNous:alpha Sep 2, 2025
4 checks passed
@sunsky89757

Copy link
Copy Markdown

{
"text": "你好世界。",
"usage": {
"type": "tokens",
"total_tokens": 10,
"input_tokens": 10,
"input_token_details": {
"text_tokens": 0,
"audio_tokens": 10
},
"output_tokens": 0
}
}

返回是0,难道是az版本的问题么?

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