Skip to content

feat(asr): 支持 OpenRouter 作为 ASR 提供商 (#582)#591

Merged
H-Chris233 merged 1 commit into
betafrom
feat/issue-582-openrouter-asr
Jun 4, 2026
Merged

feat(asr): 支持 OpenRouter 作为 ASR 提供商 (#582)#591
H-Chris233 merged 1 commit into
betafrom
feat/issue-582-openrouter-asr

Conversation

@appergb
Copy link
Copy Markdown
Collaborator

@appergb appergb commented Jun 4, 2026

User description

需求

Closes #582

OpenRouter 的 /audio/transcriptions 与 OpenAI 路径相同、也走 Bearer 鉴权,但请求体格式不同:

OpenAI / 现有兼容厂商 OpenRouter
Content-Type multipart/form-data application/json
音频 part file = WAV 二进制 input_audio.data = base64(wav)

所以直接把 OpenRouter 当 whisper 兼容 provider 加进预设,multipart 请求会被拒。

改动(单一职责:接入 OpenRouter ASR)

后端

  • whisper.rs:新增 AsrRequestFormat { Multipart, OpenRouterJson }transcribe_chunk 按格式分发。OpenRouterJson{model, input_audio:{data, format:"wav"}},用标准 base64(带 padding),且不带 multipart 专属的 prompt/response_format 字段(避免未知字段 4xx)。用 with_request_format builder,不改 new() 签名(既有 4 处构造点零改动)。
  • coordinator.rsis_whisper_compatible_provider 注册 openrouter;新增 whisper_request_formatverbose_json 对 openrouter 保持关闭(其 JSON 协议不吃 response_format);base64 膨胀 ~33%,长录音按 30s 切分(同 zhipu)。

前端

  • ASR_PRESETS / AsrPresetId / Overview 名称映射 + 5 份 i18n 加 openrouter,默认 baseUrl=https://openrouter.ai/api/v1model=openai/whisper-large-v3-turbo(报告人已验证的模型)。

测试

  • whisper.rs:OpenRouterJson 发 JSON body(含 input_audio + format:"wav"、非 multipart)的单测;既有 multipart transcribe 测试不变。
  • coordinator.rs:注册/格式/verbose_json/chunk-limit 断言。

验证

  • tsc --noEmit 通过
  • cargo check 通过
  • cargo test --lib(openrouter + transcribe 共 11 项)全过
  • 真机:用真实 OpenRouter key 验证响应体字段 ({text}) 与长录音切分上限

依赖

新增直接依赖 base64 = "0.22"(此前已是传递依赖,未引入新 crate)。

备注

OpenRouter 官方文档已确认请求/响应格式(响应顶层 text,与 OpenAI 一致),故复用现有 json["text"] 解析。

@claude 请审核。


PR Type

Enhancement, Tests


Description

  • Add OpenRouter ASR provider with JSON+base64 format

  • Backend: new request format enum, chunk limit, base64 dependency

  • Frontend: preset, i18n, type definitions

  • Tests: verify JSON request and provider registration


Diagram Walkthrough

flowchart LR
  User["User selects OpenRouter"] --> Frontend["ASR_PRESETS + i18n"]
  Frontend --> Coordinator["coordinator.rs: set request format"]
  Coordinator --> WhisperBatch["WhisperBatchASR"]
  WhisperBatch --> OpenRouterJson["OpenRouterJson encoding"]
  OpenRouterJson --> JSON["application/json body"]
  JSON --> Audio["input_audio.data: base64(wav)"]
  Audio --> Response["{ text }"]
Loading

File Walkthrough

Relevant files
Enhancement
6 files
whisper.rs
Add AsrRequestFormat enum and OpenRouterJson encoding       
+133/-30
coordinator.rs
Register openrouter as whisper compatible provider and set format
+45/-10 
dictation.rs
Use with_request_format for dictation session                       
+12/-8   
Overview.tsx
Add openrouter to ASR name key mapping                                     
+1/-0     
ProvidersSection.tsx
Add OpenRouter preset with default model and baseUrl         
+3/-0     
shared.tsx
Add openrouter to AsrPresetId union                                           
+1/-0     
Documentation
5 files
en.ts
Add asrOpenrouter i18n key                                                             
+1/-0     
ja.ts
Add asrOpenrouter i18n key                                                             
+1/-0     
ko.ts
Add asrOpenrouter i18n key                                                             
+1/-0     
zh-CN.ts
Add asrOpenrouter i18n key                                                             
+1/-0     
zh-TW.ts
Add asrOpenrouter i18n key                                                             
+1/-0     
Dependencies
1 files
Cargo.toml
Add base64 dependency                                                                       
+2/-0     

OpenRouter 的 /audio/transcriptions 与 OpenAI 路径相同、也走 Bearer,但请求体是
application/json:{model, input_audio:{data:<base64 wav>, format:"wav"}},而非 OpenAI
官方的 multipart/form-data。直接当 whisper 兼容 provider 会被 OpenRouter 拒绝。

- whisper.rs: 新增 AsrRequestFormat{Multipart,OpenRouterJson},transcribe_chunk 按格式
  分发;OpenRouterJson 走 JSON+标准 base64(带 padding),不带 multipart 专属的
  prompt/response_format 字段。用 with_request_format builder 避免改 new() 签名(不动
  既有 4 处构造点)。
- coordinator.rs: is_whisper_compatible_provider 注册 openrouter;新增
  whisper_request_format;verbose_json 对 openrouter 保持关闭;base64 膨胀 ~33%,长
  录音按 30s 切分(同 zhipu)。
- 前端: ASR_PRESETS / AsrPresetId / Overview 名称映射 + 5 份 i18n 加 openrouter。
- 测试: OpenRouterJson 发 JSON body 单测 + 注册/格式断言;既有 transcribe 测试不变。

默认 model 用报告人已验证的 openai/whisper-large-v3-turbo。
新增直接依赖 base64=0.22(此前已是传递依赖,未引入新 crate)。
@appergb
Copy link
Copy Markdown
Collaborator Author

appergb commented Jun 4, 2026

@claude 请审核这个 PR:新增 OpenRouter 作为 ASR provider(请求体 JSON+base64)。重点看:(1) transcribe_chunk 的 match 分支里 wav 在两臂分别 move/borrow 是否有隐患;(2) OpenRouterJson 是否正确排除了 multipart 专属字段(prompt/response_format);(3) base64 标准编码(带 padding)是否符合 OpenRouter 要求;(4) verbose_json 对 openrouter 保持关闭是否正确;(5) 长录音 30s 切分是否合理。

@claude
Copy link
Copy Markdown

claude Bot commented Jun 4, 2026

Claude finished @appergb's task in 2m 51s —— View job


PR 代码审核:OpenRouter ASR 支持

  • 建立任务列表
  • 读取核心文件 (whisper.rs, coordinator.rs, dictation.rs)
  • 审核 (1) transcribe_chunk 中 wav 的 move/borrow 问题
  • 审核 (2) OpenRouterJson 是否正确排除 multipart 专属字段
  • 审核 (3) base64 标准编码(带 padding)是否符合 OpenRouter 要求
  • 审核 (4) verbose_json 对 openrouter 保持关闭是否正确
  • 审核 (5) 长录音 30s 切分是否合理
  • 整理并发布最终审核结果

审核结果

整体实现质量良好,核心路径的正确性可以确认。以下逐点分析。


(1) transcribe_chunk 中 wav 的 move/borrow

结论:✅ 无隐患。

wav: Vec<u8> 在两臂的用法:

分支 用法 语义
Multipart Part::bytes(wav) 所有权转移(move)进 Part
OpenRouterJson STANDARD.encode(&wav) 临时借用,encode() 返回 String 后借用即结束

match 两臂互斥,Rust 借用检查器按臂独立分析所有权。OpenRouterJson 臂内 encode(&wav) 产生一个 String,该 Stringserde_json::json!() 宏值捕获形成 Value——body 不持有 &wav。之后 .json(&body) 只序列化 bodywav 在函数尾自然 drop,无任何 use-after-move / 悬垂借用风险。

cargo check / cargo test 均通过也印证了这一点。


(2) OpenRouterJson 是否正确排除 multipart 专属字段

结论:✅ 正确,且有两层保障。

whisper.rs 174–189 行,OpenRouterJson 分支的 body:

serde_json::json!({
    "model": self.model,
    "input_audio": {
        "data": base64::engine::general_purpose::STANDARD.encode(&wav),
        "format": "wav",
    },
})

该 body 仅含 model + input_audio,以下字段均未出现:

  • prompt ✅ 未发送
  • response_format ✅ 未发送
  • temperature ✅ 未发送(仅在 Multipart + verbose_json 分支)

小建议:当前测试(openrouter_format_posts_json_with_base64_audio,whisper.rs 775–829 行)只断言 body 不含 multipart/form-data,未显式验证 prompt / response_format 字段不存在。可考虑在测试里解析 JSON body 后加两条 assert!(!request_text.contains("\"prompt\"")) 以更明确地锁住这一契约。


(3) base64 标准编码(带 padding)

结论:✅ 选型正确。

base64::engine::general_purpose::STANDARD 对应 RFC 4648 §4:

  • 字符表:A–Z a–z 0–9 + /
  • 末尾补齐:= / ==(长度不整除 3 时必然出现)

这是 JSON API 传输二进制数据的惯例选择;对比 URL_SAFE- / _,通常不带 padding),STANDARD 与 OpenRouter 官方文档及大多数 API 网关更兼容。

30s WAV(16 kHz 16-bit mono)≈ 960 KB,base64 后 ≈ 1.28 MB,payload 尺寸合理。


(4) verbose_json 对 OpenRouter 保持关闭

结论:✅ 正确,双重保障。

第一层(coordinator.rs 2515–2516 行):

fn whisper_supports_verbose_json(provider_id: &str) -> bool {
    matches!(provider_id, "whisper" | "groq")  // openrouter 不在列
}

第二层(whisper.rs 174–184 行):OpenRouterJson 分支构造的 body 里根本没有 response_format 字段,即使上层误传 verbose_json=true 也不会发到线上。

响应解析走 json["text"].as_str()(非 verbose 路径),与 OpenRouter 返回的 {"text": "..."} 顶层结构完全吻合。QA 路径(coordinator.rs 2688 行)也一致调用 whisper_request_format(active_asr),行为对称。


(5) 长录音 30s 切分是否合理

结论:✅ 合理但偏保守,后续可按实测放宽。

时长 PCM base64 风险
30s 960 KB ~1.28 MB 保守,与 zhipu 对齐
60s 1.92 MB ~2.56 MB 大多数 gateway 可接受
120s 3.84 MB ~5.12 MB 接近部分反代 body 上限

30s 切分的合理性:

  1. OpenRouter 是反代层,body 限制由上游模型服务及 OR 自身共同决定,保守值安全
  2. base64 导致 33% 膨胀,相同录音比 multipart 更易触发 413
  3. 与 zhipu 的 Some(30_000) 对齐,测试路径复用既有验证

可选后续优化:真机测试通过后,如 OpenRouter 文档确认 body 上限 ≥ 8 MB,可将切分改为 60s 以减少长录音的 HTTP 往返次数。


总体评价

代码结构清晰,builder 模式(with_request_format)不破坏已有构造点签名,测试覆盖了核心分支(multipart vs JSON 格式判定、verbose_json 断言、chunk 限制)。5 个审核点中无阻塞性问题,仅有一个可选改进

建议:在 openrouter_format_posts_json_with_base64_audio 测试里,解析 JSON body 并显式断言 prompt / response_format 字段不存在,以更精确地锁住"排除 multipart 专属字段"这一契约。Fix this →

其余挂起事项(真机长录音验证)与 PR 描述一致,是合理的 TODO。

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis ✅

582 - Fully compliant

Compliant requirements:

  • 添加了 AsrRequestFormat::OpenRouterJson 分支,发送 JSON body 并包含 base64 音频
  • 在 is_whisper_compatible_provider 中注册了 openrouter
  • 新增 whisper_request_format 函数,为 openrouter 返回 OpenRouterJson
  • 前端添加了 openrouter 预设(baseUrl=https://openrouter.ai/api/v1,model=openai/whisper-large-v3-turbo)
  • 添加了对应的 i18n 条目和测试
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@H-Chris233 H-Chris233 merged commit 0d94364 into beta Jun 4, 2026
4 checks passed
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.

[feature] 希望支持OpenRouter作为ASR提供商

2 participants