|
| 1 | +// Copyright Envoy AI Gateway Authors |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | +// The full text of the Apache license is available in the LICENSE file at |
| 4 | +// the root of the repo. |
| 5 | + |
| 6 | +package translator |
| 7 | + |
| 8 | +import ( |
| 9 | + "io" |
| 10 | + "strconv" |
| 11 | + |
| 12 | + "github.com/envoyproxy/ai-gateway/internal/apischema/dashscope" |
| 13 | + "github.com/envoyproxy/ai-gateway/internal/internalapi" |
| 14 | + "github.com/envoyproxy/ai-gateway/internal/metrics" |
| 15 | + "github.com/envoyproxy/ai-gateway/internal/tracing/tracingapi" |
| 16 | +) |
| 17 | + |
| 18 | +// dashScopeVoiceEnrollmentPath is DashScope's native voice-cloning endpoint. A single URL |
| 19 | +// hosts every action (create_voice / list_voice / query_voice / update_voice / delete_voice); |
| 20 | +// the action is chosen in the request body. |
| 21 | +// |
| 22 | +// https://www.alibabacloud.com/help/en/model-studio/voice-clone-design-http-api |
| 23 | +const dashScopeVoiceEnrollmentPath = "/api/v1/services/audio/tts/customization" |
| 24 | + |
| 25 | +// DashScopeVoiceEnrollmentSpan is a zero-value span for voice-enrollment. There is no |
| 26 | +// per-request response object to record — the endpoint returns short JSON envelopes |
| 27 | +// (voice_id, status, page_index, …) that we forward untouched. |
| 28 | +type DashScopeVoiceEnrollmentSpan = tracingapi.Span[struct{}, struct{}] |
| 29 | + |
| 30 | +// DashScopeVoiceEnrollmentTranslator translates DashScope voice-enrollment requests as a |
| 31 | +// pass-through. It exists so the gateway can attach auth/logging/RBAC to the DashScope |
| 32 | +// management surface without reshaping the payload. |
| 33 | +type DashScopeVoiceEnrollmentTranslator = Translator[dashscope.VoiceEnrollmentRequest, DashScopeVoiceEnrollmentSpan] |
| 34 | + |
| 35 | +// dashScopeVoiceEnrollmentTranslator forwards voice-enrollment CRUD calls to DashScope |
| 36 | +// unchanged. The body varies per action (create takes `url`+`prefix`, delete takes only |
| 37 | +// `voice_id`, list is paginated, etc.) so any translator-side reshaping would just add |
| 38 | +// friction; we forward the caller's payload verbatim and only rewrite `:path`. |
| 39 | +type dashScopeVoiceEnrollmentTranslator struct{} |
| 40 | + |
| 41 | +// NewDashScopeVoiceEnrollmentTranslator creates a pass-through translator for voice-enrollment. |
| 42 | +func NewDashScopeVoiceEnrollmentTranslator() DashScopeVoiceEnrollmentTranslator { |
| 43 | + return &dashScopeVoiceEnrollmentTranslator{} |
| 44 | +} |
| 45 | + |
| 46 | +// RequestBody implements [DashScopeVoiceEnrollmentTranslator.RequestBody]. |
| 47 | +// |
| 48 | +// Body flows through untouched — DashScope owns the schema per action, and modelling every |
| 49 | +// variant here would just track upstream drift. The router already parsed body.model / |
| 50 | +// body.input.action for logging + routing, so nothing else is required. We only need to |
| 51 | +// re-emit the original bytes when Envoy's upstream stage forces a body replacement (retry / |
| 52 | +// streaming), and to rewrite `:path` to the DashScope native URL because Envoy is otherwise |
| 53 | +// still sitting on the client's inbound path. |
| 54 | +func (dashScopeVoiceEnrollmentTranslator) RequestBody(original []byte, _ *dashscope.VoiceEnrollmentRequest, forceBodyMutation bool) ( |
| 55 | + newHeaders []internalapi.Header, newBody []byte, err error, |
| 56 | +) { |
| 57 | + newHeaders = []internalapi.Header{{pathHeaderName, dashScopeVoiceEnrollmentPath}} |
| 58 | + if forceBodyMutation && len(original) > 0 { |
| 59 | + newBody = original |
| 60 | + newHeaders = append(newHeaders, internalapi.Header{contentLengthHeaderName, strconv.Itoa(len(newBody))}) |
| 61 | + } |
| 62 | + return |
| 63 | +} |
| 64 | + |
| 65 | +// ResponseHeaders implements [DashScopeVoiceEnrollmentTranslator.ResponseHeaders]. |
| 66 | +// DashScope returns application/json envelopes and the caller expects that shape, so we do |
| 67 | +// not touch response headers. |
| 68 | +func (dashScopeVoiceEnrollmentTranslator) ResponseHeaders(_ map[string]string) (newHeaders []internalapi.Header, err error) { |
| 69 | + return nil, nil |
| 70 | +} |
| 71 | + |
| 72 | +// ResponseBody implements [DashScopeVoiceEnrollmentTranslator.ResponseBody]. Passes DashScope's |
| 73 | +// JSON response through unchanged; the action-specific envelope (`voice_id`, `voice_list`, |
| 74 | +// resource_link, status, …) is the caller's contract, not the gateway's to reshape. |
| 75 | +func (dashScopeVoiceEnrollmentTranslator) ResponseBody(_ map[string]string, _ io.Reader, _ bool, _ DashScopeVoiceEnrollmentSpan) ( |
| 76 | + newHeaders []internalapi.Header, newBody []byte, tokenUsage metrics.TokenUsage, responseModel internalapi.ResponseModel, err error, |
| 77 | +) { |
| 78 | + return nil, nil, metrics.TokenUsage{}, "voice-enrollment", nil |
| 79 | +} |
| 80 | + |
| 81 | +// ResponseError implements [DashScopeVoiceEnrollmentTranslator.ResponseError]. DashScope's |
| 82 | +// error envelope (`code`, `message`) is passed through as-is — no OpenAI-shaped rewriting is |
| 83 | +// meaningful for a fork-only native endpoint. |
| 84 | +func (dashScopeVoiceEnrollmentTranslator) ResponseError(_ map[string]string, _ io.Reader) (newHeaders []internalapi.Header, newBody []byte, err error) { |
| 85 | + return nil, nil, nil |
| 86 | +} |
0 commit comments