Skip to content

feat(speech): expose DashScope voice-enrollment (voice cloning) API - #9

Merged
pokabookinflab merged 1 commit into
mainfrom
feature/qwen-voice-enrollment
Jul 28, 2026
Merged

feat(speech): expose DashScope voice-enrollment (voice cloning) API#9
pokabookinflab merged 1 commit into
mainfrom
feature/qwen-voice-enrollment

Conversation

@pokabookinflab

@pokabookinflab pokabookinflab commented Jul 28, 2026

Copy link
Copy Markdown

Summary

Adds POST /api/v1/services/audio/tts/customization as a fork-only, native DashScope endpoint so voice-cloning management (create_voice, list_voice, query_voice, update_voice, delete_voice) flows through the gateway with the usual auth / logging / RBAC surface attached. Depends on the AlibabaDashScope schema shipped in #6.

Design — pass-through, not translation

DashScope's voice-cloning API has no OpenAI counterpart — the caller must speak DashScope's native schema ({"model":"voice-enrollment","input":{"action":"create_voice",...}}). Every action shares the same URL and differs only by input.action. We would gain nothing by modelling each action's fields on our side; the schema would just chase upstream drift.

So the gateway:

  • Parses only body.model (→ x-ai-eg-model=voice-enrollment for route matching) and body.input.action (available for metrics tagging).
  • Forwards the rest of the body verbatim.
  • Rewrites :path to /api/v1/services/audio/tts/customization.
  • Returns DashScope's JSON response unchanged (voice_id, voice_list, resource_link, …).

Files

file purpose
internal/apischema/dashscope/dashscope.go VoiceEnrollmentRequest + VoiceEnrollmentInput minimal parsing types
internal/translator/voice_enrollment_dashscope.go pass-through translator (path rewrite + body replay only on forced mutation)
internal/translator/voice_enrollment_dashscope_test.go unit tests for path/body/response/error surfaces
internal/endpointspec/endpointspec.go DashScopeVoiceEnrollmentEndpointSpec with ParseBody/GetTranslator/redaction
cmd/extproc/mainlib/main.go server.Register for the native path (speech metrics + noop tracer)

Not in this PR

  • gitops route + backend: separate follow-up so the gateway image can roll independently of the route change. Route rule matches x-ai-eg-model=voice-enrollment → new AIServiceBackend pointing at the workspace endpoint (ws-…-maas.aliyuncs.com) with schema.name: AlibabaDashScope.
  • OpenAI-shaped surface (/v1/audio/voices etc.). DashScope's action-set doesn't map cleanly to any OpenAI shape today.

Verified

  • go build ./... && go vet ./...
  • golangci-lint run → 0 issues
  • Full test pass: internal/translator, internal/endpointspec, internal/extproc

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 새 기능

    • Alibaba DashScope의 음성 등록 및 음성 복제 관리 API를 지원합니다.
    • 음성 등록 요청과 결과, 오류 응답을 원본 형식에 가깝게 전달합니다.
    • 음성 등록 요청의 작업 유형을 지정할 수 있습니다.
  • 테스트

    • 요청 전달, 응답 처리, 오류 처리 동작을 검증하는 테스트를 추가했습니다.

Adds POST /api/v1/services/audio/tts/customization as a fork-only, native
DashScope endpoint so voice-cloning management (create_voice, list_voice,
query_voice, update_voice, delete_voice) flows through the gateway with the
usual auth / logging / RBAC surface attached.

Design: this is a pass-through, not a translation. There is no OpenAI-shaped
counterpart to voice-cloning; the caller speaks DashScope's native schema and
the gateway forwards the body unchanged. Only body.model is parsed (to
populate x-ai-eg-model=voice-enrollment for route matching) and body.input.action
is exposed on the request struct so operators can tag metrics per action.

Layout mirrors the existing GeminiCachedContentsEndpointSpec / cachedContents
translator pair:

- internal/apischema/dashscope: VoiceEnrollmentRequest + VoiceEnrollmentInput
  minimal parsing types (Model + Action). Rest of the payload stays as raw
  JSON because it varies per action and modelling it here would just track
  upstream drift.
- internal/translator/voice_enrollment_dashscope.go: pass-through translator
  that rewrites `:path` to /api/v1/services/audio/tts/customization and only
  replays the body when Envoy's upstream stage forces a mutation (retry /
  streaming). No response reshaping — DashScope's action-specific envelope is
  the caller's contract.
- internal/endpointspec/endpointspec.go: DashScopeVoiceEnrollmentEndpointSpec
  with ParseBody / GetTranslator / RedactSensitiveInfoFromRequest. Only
  AlibabaDashScope schema is supported.
- cmd/extproc/mainlib/main.go: server.Register for the native path with a
  noop tracer (no response object to record) and speech metrics factory.

Not covered by this PR (defer):

- gitops backend + route rule (workspace endpoint + AIGatewayRoute) — separate
  follow-up so the gateway image can roll independently of the route change.
- OpenAI-shaped surface (/v1/audio/voices etc.). If demand shows up, layer it
  on top; DashScope's action-set doesn't map cleanly to any OpenAI shape today.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

DashScope 음성 등록 요청 스키마와 전용 엔드포인트 스펙이 추가되었습니다. 번역기는 네이티브 경로로 요청을 전달하고 응답·오류를 패스스루하며, 외부 프로세서에 새 라우팅 경로가 등록되었습니다.

Changes

DashScope 음성 등록

Layer / File(s) Summary
요청 계약과 엔드포인트 스펙
internal/apischema/dashscope/dashscope.go, internal/endpointspec/endpointspec.go
VoiceEnrollmentRequest와 입력 구조체를 추가하고, JSON 파싱·DashScope 백엔드 제한·멀티파트 비지원 동작을 구현했습니다.
패스스루 번역과 검증
internal/translator/voice_enrollment_dashscope.go, internal/translator/voice_enrollment_dashscope_test.go
네이티브 음성 등록 경로로 요청을 전달하고 응답·오류 본문을 유지하며, 요청 경로와 콘텐츠 길이 및 응답 메타데이터 동작을 테스트합니다.
외부 프로세서 라우팅
cmd/extproc/mainlib/main.go
/api/v1/services/audio/tts/customization 경로에 전용 스펙, 메트릭 팩토리, noop tracer를 연결했습니다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ExtProc
  participant EndpointSpec
  participant Translator
  participant DashScope
  Client->>ExtProc: POST 음성 등록 요청
  ExtProc->>EndpointSpec: 요청 파싱 및 백엔드 확인
  EndpointSpec->>Translator: 번역기 선택
  Translator->>DashScope: 네이티브 경로와 요청 본문 전달
  DashScope-->>Translator: 응답 또는 오류 반환
  Translator-->>Client: 원본 응답 또는 오류 전달
Loading

Possibly related PRs

  • inflearn/ai-gateway#6: 동일한 DashScope 스키마 모듈과 Qwen-TTS 엔드포인트 계열을 확장합니다.

Poem

당근을 문 채 새 길을 열었네
음성 등록 요청이 달려가네
경로는 DashScope로 곧게 뻗고
응답은 그대로 깡충 돌아오네
토끼도 박수, 깡총깡총! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 변경 사항이 DashScope voice-enrollment(voice cloning) API 엔드포인트 노출과 정확히 일치합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/qwen-voice-enrollment

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/endpointspec/endpointspec.go`:
- Around line 617-624: Validate req.Model in the voice-enrollment request
handling before returning from this path: require the exact value
"voice-enrollment", reject empty or mismatched values with
internalapi.ErrMalformedRequest, and return the fixed model value for routing
instead of arbitrary req.Model.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 375a61da-79eb-4f3a-b56b-ef824dcdd6c6

📥 Commits

Reviewing files that changed from the base of the PR and between b6e8a4e and 9a63879.

📒 Files selected for processing (5)
  • cmd/extproc/mainlib/main.go
  • internal/apischema/dashscope/dashscope.go
  • internal/endpointspec/endpointspec.go
  • internal/translator/voice_enrollment_dashscope.go
  • internal/translator/voice_enrollment_dashscope_test.go

Comment on lines +617 to +624
var req dashscopeschema.VoiceEnrollmentRequest
if len(body) > 0 {
if err := json.Unmarshal(body, &req); err != nil {
return "", nil, false, nil, fmt.Errorf("%w: failed to parse JSON for voice enrollment: %w", internalapi.ErrMalformedRequest, err)
}
}
// voice-enrollment is never streamed.
return req.Model, &req, false, nil, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

modelvoice-enrollment으로 검증해야 합니다.

주석과 테스트는 이 엔드포인트의 모델이 항상 voice-enrollment이라고 정의하지만, 현재 구현은 임의의 req.Model을 그대로 라우팅에 반환합니다. 이 값이 x-ai-eg-model 및 모델별 라우팅/RBAC 선택에 사용되므로, 불일치하거나 빈 model은 ErrMalformedRequest로 거부하고 고정값만 허용해야 합니다.

제안 수정
 	if len(body) > 0 {
 		if err := json.Unmarshal(body, &req); err != nil {
 			return "", nil, false, nil, fmt.Errorf("%w: failed to parse JSON for voice enrollment: %w", internalapi.ErrMalformedRequest, err)
 		}
 	}
+	if req.Model != "voice-enrollment" {
+		return "", nil, false, nil, fmt.Errorf("%w: model must be voice-enrollment", internalapi.ErrMalformedRequest)
+	}
 	// voice-enrollment is never streamed.
📝 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 req dashscopeschema.VoiceEnrollmentRequest
if len(body) > 0 {
if err := json.Unmarshal(body, &req); err != nil {
return "", nil, false, nil, fmt.Errorf("%w: failed to parse JSON for voice enrollment: %w", internalapi.ErrMalformedRequest, err)
}
}
// voice-enrollment is never streamed.
return req.Model, &req, false, nil, nil
var req dashscopeschema.VoiceEnrollmentRequest
if len(body) > 0 {
if err := json.Unmarshal(body, &req); err != nil {
return "", nil, false, nil, fmt.Errorf("%w: failed to parse JSON for voice enrollment: %w", internalapi.ErrMalformedRequest, err)
}
}
if req.Model != "voice-enrollment" {
return "", nil, false, nil, fmt.Errorf("%w: model must be voice-enrollment", internalapi.ErrMalformedRequest)
}
// voice-enrollment is never streamed.
return req.Model, &req, false, nil, nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/endpointspec/endpointspec.go` around lines 617 - 624, Validate
req.Model in the voice-enrollment request handling before returning from this
path: require the exact value "voice-enrollment", reject empty or mismatched
values with internalapi.ErrMalformedRequest, and return the fixed model value
for routing instead of arbitrary req.Model.

@pokabookinflab
pokabookinflab merged commit d10579c into main Jul 28, 2026
26 of 32 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.

1 participant