원클릭으로
aigw-contrib-add-translator
Add a new LLM provider translator to envoyproxy/ai-gateway — the most common contribution type
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Add a new LLM provider translator to envoyproxy/ai-gateway — the most common contribution type
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Configure client-facing traffic policies -- timeouts, connection limits, TLS settings, HTTP behavior
Production-grade Envoy Gateway setup with comprehensive security, observability, high availability, and operational best practices
Integrate Envoy Gateway with Istio ambient mesh or Cilium for unified ingress and service mesh
Envoy Gateway version information, compatibility matrix, and upgrade readiness checks
Envoy AI Gateway contribution orchestrator — interviews you about your contribution and guides you through the correct workflow using contributor skills
Envoy Gateway contribution orchestrator — interviews you about your contribution and guides you through the correct workflow using contributor skills
| name | aigw-contrib-add-translator |
| description | Add a new LLM provider translator to envoyproxy/ai-gateway — the most common contribution type |
| arguments | [{"name":"SourceSchema","description":"Input API schema (e.g., OpenAI, Anthropic)","required":true},{"name":"TargetSchema","description":"Output API schema (e.g., AWSBedrock, AzureOpenAI, GCPVertexAI)","required":true},{"name":"ProviderName","description":"Human-readable provider name (e.g., AWS Bedrock, Azure OpenAI)","required":true}] |
Add a new LLM provider translator to Envoy AI Gateway. This is the most common contribution type. A translator converts requests from one API schema (e.g., OpenAI) to another (e.g., AWS Bedrock) and translates responses back.
Before starting: Read aigw-contrib-go-style — performance patterns and JSON handling are the most common reasons translator PRs get rejected.
Study an existing translator to understand the pattern. Start with a simple one like openai_openai.go (passthrough), then look at a complex one like openai_awsbedrock.go.
Key interfaces your translator must implement:
(sourceBody []byte) → (targetBody []byte, error)(targetBody []byte) → (sourceBody []byte, error)(targetChunk []byte) → (sourceChunk []byte, error)The translator is generic over [ReqT, RespT, RespChunkT] type parameters.
Create internal/extproc/translators/${SourceSchema}_${TargetSchema}.go:
// Copyright Envoy AI Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.
package translators
import (
"fmt"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"github.com/envoyproxy/ai-gateway/internal/json"
)
// ${SourceSchema}To${TargetSchema}Translator translates from ${SourceSchema}
// to ${TargetSchema} API format.
type ${sourceSchema}To${targetSchema}Translator struct {
// Pre-allocated resources — constructed once, not per-request
}
// new${SourceSchema}To${TargetSchema}Translator creates a new translator.
func new${SourceSchema}To${TargetSchema}Translator() *${sourceSchema}To${targetSchema}Translator {
return &${sourceSchema}To${targetSchema}Translator{}
}
gjson to read JSON fields — never unmarshal to map[string]interface{}sjson to mutate JSON bytes — never marshal from mapsinternal/json for marshal/unmarshal — never encoding/jsonfunc (t *${sourceSchema}To${targetSchema}Translator) translateRequest(
body []byte,
) (translated []byte, err error) {
// Extract model from source format
model := gjson.GetBytes(body, "model").String()
if model == "" {
return nil, fmt.Errorf("missing model field in request")
}
// Transform to target format
translated = body // Start with source, modify as needed
translated, err = sjson.SetBytes(translated, "modelId", model)
if err != nil {
return nil, fmt.Errorf("setting modelId: %w", err)
}
translated, err = sjson.DeleteBytes(translated, "model")
if err != nil {
return nil, fmt.Errorf("deleting model: %w", err)
}
return translated, nil
}
var (
sseDataPrefix = []byte("data: ")
sseLineEnd = []byte("\n\n")
sseDoneMessage = []byte("data: [DONE]\n\n")
)
func (t *${sourceSchema}To${targetSchema}Translator) translateStreamChunk(
chunk []byte,
) (out []byte, err error) {
// Handle [DONE] sentinel
if bytes.Equal(bytes.TrimSpace(chunk), []byte("[DONE]")) {
return sseDoneMessage, nil
}
// Parse target format chunk
// Transform to source format chunk
// Build SSE output
out = append(out, sseDataPrefix...)
out = append(out, translatedChunk...)
out = append(out, sseLineEnd...)
return out, nil
}
Register your translator in the EndpointSpec path-based lookup. The exact registration location depends on the current codebase structure — look for where existing translators are registered (typically in the router or upstream processor initialization).
The registration maps:
/v1/chat/completions) + source schema + target schema → your translatorIf the target provider needs a new authentication method not already supported:
internal/extproc/backendauth/api/v1alpha1/backend_security_policy_types.go (separate API PR)Existing auth types: APIKey, AnthropicAPIKey, AzureAPIKey, AWSCredentials, AzureCredentials, GCPCredentials.
If the new translator introduces a new VersionedAPISchema value:
api/v1alpha1/filterapi/filterapi.goCreate internal/extproc/translators/${SourceSchema}_${TargetSchema}_test.go:
// Copyright Envoy AI Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.
package translators
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test${SourceSchema}To${TargetSchema}_TranslateRequest(t *testing.T) {
translator := new${SourceSchema}To${TargetSchema}Translator()
tests := []struct {
name string
input []byte
expOutput []byte
expErr string
}{
{
name: "valid chat completion",
input: []byte(`{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}`),
expOutput: []byte(`{...target format...}`),
},
{
name: "missing model",
input: []byte(`{"messages":[{"role":"user","content":"hello"}]}`),
expErr: "missing model",
},
{
name: "with streaming option",
input: []byte(`{"model":"gpt-4o","stream":true,"messages":[...]}`),
expOutput: []byte(`{...target with streaming...}`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
output, err := translator.translateRequest(tc.input)
if tc.expErr != "" {
require.ErrorContains(t, err, tc.expErr)
return
}
require.NoError(t, err)
require.JSONEq(t, string(tc.expOutput), string(output))
})
}
}
func Test${SourceSchema}To${TargetSchema}_TranslateStreamChunk(t *testing.T) {
translator := new${SourceSchema}To${TargetSchema}Translator()
tests := []struct {
name string
chunk []byte
expOutput []byte
expErr string
}{
{
name: "normal data chunk",
chunk: []byte(`{...target streaming chunk...}`),
expOutput: []byte("data: {...source streaming chunk...}\n\n"),
},
{
name: "done sentinel",
chunk: []byte("[DONE]"),
expOutput: []byte("data: [DONE]\n\n"),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
output, err := translator.translateStreamChunk(tc.chunk)
if tc.expErr != "" {
require.ErrorContains(t, err, tc.expErr)
return
}
require.NoError(t, err)
require.Equal(t, string(tc.expOutput), string(output))
})
}
}
For more realistic tests, use go-vcr to record real API interactions:
func TestWithRealProvider(t *testing.T) {
r, err := recorder.New("testdata/fixtures/${SourceSchema}-to-${TargetSchema}")
require.NoError(t, err)
defer r.Stop()
// Use r as HTTP transport — records/replays real responses
}
Add tests in tests/data-plane/ that run your translator through a real Envoy:
//go:build test_data_plane
func Test${SourceSchema}To${TargetSchema}_DataPlane(t *testing.T) {
// 1. Write filterapi.Config with new schema mapping
// 2. Start ExtProc server
// 3. Start Envoy via func-e
// 4. Send ${SourceSchema}-format request
// 5. Verify backend receives ${TargetSchema} format
// 6. Verify response translated back to ${SourceSchema}
// 7. Test streaming SSE path
}
Run: make test-data-plane
internal/extproc/translators/${SourceSchema}_${TargetSchema}.gogjson/sjson for JSON ops — not encoding/json or mapsinternal/json for marshal/unmarshal[DONE] sentinel handled correctlymake precommit passesmake test passesfeat(extproc): add ${SourceSchema} to ${TargetSchema} translator