Skip to content

Commit 1245a8d

Browse files
authored
feat: complete the payload processing move (#602)
<!--- Provide a general summary of your changes in the Title above --> ## Description This PR completes the payload processing move from its own repo to MaaS repo. ## How Has This Been Tested? <!--- Please describe in detail how you tested your changes. --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> ## Merge criteria: <!--- This PR will be merged by any repository approver when it meets all the points in the checklist --> <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> - [ ] The commits are squashed in a cohesive manner and have meaningful messages. - [ ] Testing instructions have been added in the PR body (for PRs involving changes that are not immediately obvious). - [ ] The developer has manually tested the changes and verified that the changes work <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Activated API key injection plugin for enhanced request security * Enabled support for Azure OpenAI translator * Enabled support for Vertex AI translator * **Tests** * Added comprehensive test coverage for Azure OpenAI request and response handling * Added comprehensive test coverage for Vertex request processing, response transformation, error handling, and tool-call translation <!-- end of auto-generated comment: release notes by coderabbit.ai --> Signed-off-by: Nir Rozenbaum <nrozenba@redhat.com>
1 parent 6df5036 commit 1245a8d

File tree

17 files changed

+253
-29
lines changed

17 files changed

+253
-29
lines changed

payload-processing/cmd/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2025.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -30,8 +30,8 @@ import (
3030
"sigs.k8s.io/gateway-api-inference-extension/pkg/bbr/framework"
3131

3232
api_translation "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation"
33+
apikey_injection "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/apikey-injection"
3334
provider_resolver "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/model-provider-resolver"
34-
// apikey_injection "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/apikey-injection"
3535
)
3636

3737
func main() {
@@ -48,5 +48,5 @@ func main() {
4848
func registerPlugins() {
4949
framework.Register(provider_resolver.ModelProviderResolverPluginType, provider_resolver.ModelProviderResolverFactory)
5050
framework.Register(api_translation.APITranslationPluginType, api_translation.APITranslationFactory)
51-
// framework.Register(apikey_injection.APIKeyInjectionPluginType, apikey_injection.APIKeyInjectionFactory)
51+
framework.Register(apikey_injection.APIKeyInjectionPluginType, apikey_injection.APIKeyInjectionFactory)
5252
}

payload-processing/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.25.0
44

55
require (
66
github.com/stretchr/testify v1.11.1
7+
k8s.io/api v0.35.2
78
k8s.io/apimachinery v0.35.2
89
sigs.k8s.io/controller-runtime v0.23.3
910
sigs.k8s.io/gateway-api-inference-extension v0.0.0-20260318135032-876ac9d909d0
@@ -93,7 +94,6 @@ require (
9394
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
9495
gopkg.in/inf.v0 v0.9.1 // indirect
9596
gopkg.in/yaml.v3 v3.0.1 // indirect
96-
k8s.io/api v0.35.2 // indirect
9797
k8s.io/apiextensions-apiserver v0.35.2 // indirect
9898
k8s.io/apiserver v0.35.2 // indirect
9999
k8s.io/client-go v0.35.2 // indirect

payload-processing/pkg/plugins/api-translation/plugin.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2026 The opendatahub.io Authors.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -26,8 +26,8 @@ import (
2626

2727
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation/translator"
2828
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation/translator/anthropic"
29-
// "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation/translator/azureopenai"
30-
// "github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation/translator/vertex"
29+
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation/translator/azureopenai"
30+
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation/translator/vertex"
3131
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/common/provider"
3232
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/common/state"
3333
)
@@ -53,9 +53,9 @@ func NewAPITranslationPlugin() *APITranslationPlugin {
5353
Name: APITranslationPluginType,
5454
},
5555
providers: map[string]translator.Translator{
56-
provider.Anthropic: anthropic.NewAnthropicTranslator(),
57-
// provider.AzureOpenAI: azureopenai.NewAzureOpenAITranslator(),
58-
// provider.Vertex: vertex.NewVertexTranslator(),
56+
provider.Anthropic: anthropic.NewAnthropicTranslator(),
57+
provider.AzureOpenAI: azureopenai.NewAzureOpenAITranslator(),
58+
provider.Vertex: vertex.NewVertexTranslator(),
5959
},
6060
}
6161
}

payload-processing/pkg/plugins/api-translation/plugin_test.go

Lines changed: 223 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2026 The opendatahub.io Authors.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -95,8 +95,73 @@ func TestProcessRequest_AnthropicProvider(t *testing.T) {
9595
assert.NotContains(t, removed, "content-length")
9696
}
9797

98-
// Azure OpenAI and Vertex tests are commented out until their translators are moved to this repo.
99-
// See: TestProcessRequest_AzureOpenAIProvider, TestProcessResponse_AzureOpenAI
98+
func TestProcessRequest_AzureOpenAIProvider(t *testing.T) {
99+
p := NewAPITranslationPlugin()
100+
101+
cs := newCycleStateWithProvider("azure-openai")
102+
req := framework.NewInferenceRequest()
103+
req.Headers["authorization"] = "Bearer sk-test"
104+
req.Headers["content-length"] = "200"
105+
req.Body["model"] = "gpt-4o"
106+
req.Body["messages"] = []any{
107+
map[string]any{"role": "system", "content": "Be concise"},
108+
map[string]any{"role": "user", "content": "What is 2+2?"},
109+
}
110+
req.Body["max_tokens"] = float64(100)
111+
req.Body["temperature"] = 0.7
112+
113+
err := p.ProcessRequest(context.Background(), cs, req)
114+
require.NoError(t, err)
115+
116+
// Azure OpenAI does not mutate the body — same schema as OpenAI
117+
assert.False(t, req.BodyMutated())
118+
119+
// Original body fields are preserved
120+
assert.Equal(t, "gpt-4o", req.Body["model"])
121+
assert.Equal(t, float64(100), req.Body["max_tokens"])
122+
assert.Equal(t, 0.7, req.Body["temperature"])
123+
124+
mutated := req.MutatedHeaders()
125+
assert.Equal(t, "/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21", mutated[":path"])
126+
assert.Equal(t, "application/json", mutated["content-type"])
127+
128+
removed := req.RemovedHeaders()
129+
assert.Contains(t, removed, "authorization")
130+
assert.NotContains(t, removed, "content-length")
131+
}
132+
133+
func TestProcessResponse_AzureOpenAI(t *testing.T) {
134+
p := NewAPITranslationPlugin()
135+
136+
cs := newCycleStateWithProvider("azure-openai")
137+
cs.Write(state.ModelKey, "gpt-4o")
138+
139+
resp := framework.NewInferenceResponse()
140+
resp.Body["id"] = "chatcmpl-abc123"
141+
resp.Body["object"] = "chat.completion"
142+
resp.Body["model"] = "gpt-4o"
143+
resp.Body["choices"] = []any{
144+
map[string]any{
145+
"index": float64(0),
146+
"message": map[string]any{
147+
"role": "assistant",
148+
"content": "The answer is 4.",
149+
},
150+
"finish_reason": "stop",
151+
},
152+
}
153+
resp.Body["usage"] = map[string]any{
154+
"prompt_tokens": float64(10),
155+
"completion_tokens": float64(5),
156+
"total_tokens": float64(15),
157+
}
158+
159+
err := p.ProcessResponse(context.Background(), cs, resp)
160+
require.NoError(t, err)
161+
162+
// Azure OpenAI responses are already in OpenAI format — no mutation
163+
assert.False(t, resp.BodyMutated())
164+
}
100165

101166
func TestProcessRequest_UnknownProvider(t *testing.T) {
102167
p := NewAPITranslationPlugin()
@@ -228,9 +293,161 @@ func TestProcessResponse_AnthropicToolUse(t *testing.T) {
228293
assert.Equal(t, 0, tc["index"])
229294
}
230295

231-
// Vertex tests are commented out until the Vertex translator is moved to this repo.
232-
// See: TestProcessRequest_VertexProvider, TestProcessResponse_Vertex,
233-
// TestProcessResponse_VertexError, TestProcessResponse_VertexToolCall
296+
func TestProcessRequest_VertexProvider(t *testing.T) {
297+
p := NewAPITranslationPlugin()
298+
299+
cs := newCycleStateWithProvider("vertex")
300+
req := framework.NewInferenceRequest()
301+
req.Headers["authorization"] = "Bearer sk-test"
302+
req.Headers["content-length"] = "200"
303+
req.Body["model"] = "gemini-2.5-flash"
304+
req.Body["messages"] = []any{
305+
map[string]any{"role": "system", "content": "Be concise"},
306+
map[string]any{"role": "user", "content": "What is Kubernetes?"},
307+
}
308+
req.Body["max_tokens"] = float64(100)
309+
req.Body["temperature"] = 0.7
310+
311+
err := p.ProcessRequest(context.Background(), cs, req)
312+
require.NoError(t, err)
313+
314+
assert.True(t, req.BodyMutated())
315+
316+
contents, ok := req.Body["contents"].([]map[string]any)
317+
require.True(t, ok)
318+
require.Len(t, contents, 1)
319+
assert.Equal(t, "user", contents[0]["role"])
320+
321+
sysInstruction, ok := req.Body["systemInstruction"].(map[string]any)
322+
require.True(t, ok)
323+
sysParts := sysInstruction["parts"].([]map[string]any)
324+
assert.Equal(t, "Be concise", sysParts[0]["text"])
325+
326+
genConfig, ok := req.Body["generationConfig"].(map[string]any)
327+
require.True(t, ok)
328+
assert.Equal(t, 100, genConfig["maxOutputTokens"])
329+
assert.Equal(t, 0.7, genConfig["temperature"])
330+
331+
mutated := req.MutatedHeaders()
332+
assert.Equal(t, "/v1beta/models/gemini-2.5-flash:generateContent", mutated[":path"])
333+
assert.Equal(t, "application/json", mutated["content-type"])
334+
335+
removed := req.RemovedHeaders()
336+
assert.Contains(t, removed, "authorization")
337+
assert.NotContains(t, removed, "content-length")
338+
}
339+
340+
func TestProcessResponse_Vertex(t *testing.T) {
341+
p := NewAPITranslationPlugin()
342+
343+
cs := newCycleStateWithProvider("vertex")
344+
cs.Write(state.ModelKey, "gemini-2.5-flash")
345+
346+
resp := framework.NewInferenceResponse()
347+
resp.Body["candidates"] = []any{
348+
map[string]any{
349+
"content": map[string]any{
350+
"parts": []any{map[string]any{"text": "Kubernetes is a container orchestration platform."}},
351+
"role": "model",
352+
},
353+
"finishReason": "STOP",
354+
},
355+
}
356+
resp.Body["usageMetadata"] = map[string]any{
357+
"promptTokenCount": float64(12),
358+
"candidatesTokenCount": float64(8),
359+
"totalTokenCount": float64(20),
360+
}
361+
resp.Body["responseId"] = "resp-abc123"
362+
363+
err := p.ProcessResponse(context.Background(), cs, resp)
364+
require.NoError(t, err)
365+
366+
assert.True(t, resp.BodyMutated())
367+
assert.Equal(t, "chat.completion", resp.Body["object"])
368+
assert.Equal(t, "gemini-2.5-flash", resp.Body["model"])
369+
assert.Equal(t, "resp-abc123", resp.Body["id"])
370+
371+
choices := resp.Body["choices"].([]any)
372+
choice := choices[0].(map[string]any)
373+
msg := choice["message"].(map[string]any)
374+
assert.Equal(t, "assistant", msg["role"])
375+
assert.Equal(t, "Kubernetes is a container orchestration platform.", msg["content"])
376+
assert.Equal(t, "stop", choice["finish_reason"])
377+
378+
usage := resp.Body["usage"].(map[string]any)
379+
assert.Equal(t, 12, usage["prompt_tokens"])
380+
assert.Equal(t, 8, usage["completion_tokens"])
381+
assert.Equal(t, 20, usage["total_tokens"])
382+
}
383+
384+
func TestProcessResponse_VertexError(t *testing.T) {
385+
p := NewAPITranslationPlugin()
386+
387+
cs := newCycleStateWithProvider("vertex")
388+
389+
resp := framework.NewInferenceResponse()
390+
resp.Body["error"] = map[string]any{
391+
"code": float64(400),
392+
"message": "Invalid value at 'contents'",
393+
"status": "INVALID_ARGUMENT",
394+
}
395+
396+
err := p.ProcessResponse(context.Background(), cs, resp)
397+
require.NoError(t, err)
398+
399+
assert.True(t, resp.BodyMutated())
400+
errObj := resp.Body["error"].(map[string]any)
401+
assert.Equal(t, "INVALID_ARGUMENT", errObj["type"])
402+
assert.Equal(t, "Invalid value at 'contents'", errObj["message"])
403+
}
404+
405+
func TestProcessResponse_VertexToolCall(t *testing.T) {
406+
p := NewAPITranslationPlugin()
407+
408+
cs := newCycleStateWithProvider("vertex")
409+
cs.Write(state.ModelKey, "gemini-2.5-flash")
410+
411+
resp := framework.NewInferenceResponse()
412+
resp.Body["candidates"] = []any{
413+
map[string]any{
414+
"content": map[string]any{
415+
"parts": []any{
416+
map[string]any{
417+
"functionCall": map[string]any{
418+
"name": "get_weather",
419+
"args": map[string]any{"location": "Paris"},
420+
},
421+
},
422+
},
423+
"role": "model",
424+
},
425+
"finishReason": "STOP",
426+
},
427+
}
428+
resp.Body["usageMetadata"] = map[string]any{
429+
"promptTokenCount": float64(20),
430+
"candidatesTokenCount": float64(10),
431+
"totalTokenCount": float64(30),
432+
}
433+
434+
err := p.ProcessResponse(context.Background(), cs, resp)
435+
require.NoError(t, err)
436+
437+
assert.True(t, resp.BodyMutated())
438+
439+
choices := resp.Body["choices"].([]any)
440+
choice := choices[0].(map[string]any)
441+
msg := choice["message"].(map[string]any)
442+
toolCalls := msg["tool_calls"].([]any)
443+
require.Len(t, toolCalls, 1)
444+
445+
tc := toolCalls[0].(map[string]any)
446+
assert.Equal(t, "call_0", tc["id"])
447+
assert.Equal(t, 0, tc["index"])
448+
fn := tc["function"].(map[string]any)
449+
assert.Equal(t, "get_weather", fn["name"])
450+
}
234451

235452
func TestProcessResponse_NoProviderPassthrough(t *testing.T) {
236453
p := NewAPITranslationPlugin()

payload-processing/pkg/plugins/api-translation/translator/anthropic/anthropic.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2026 The opendatahub.io Authors.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -34,6 +34,7 @@ const (
3434
// compile-time interface check
3535
var _ translator.Translator = &AnthropicTranslator{}
3636

37+
// NewAnthropicTranslator initializes a new AnthropicTranslator and returns its pointer.
3738
func NewAnthropicTranslator() *AnthropicTranslator {
3839
return &AnthropicTranslator{}
3940
}

payload-processing/pkg/plugins/api-translation/translator/anthropic/anthropic_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2026 The opendatahub.io Authors.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.

payload-processing/pkg/plugins/api-translation/translator/azureopenai/azureopenai.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package azureopenai
1919
import (
2020
"fmt"
2121
"regexp"
22+
23+
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation/translator"
2224
)
2325

2426
const (
@@ -33,8 +35,9 @@ const (
3335
)
3436

3537
// compile-time interface check
36-
// var _ translator.Translator = &AzureOpenAITranslator{}
38+
var _ translator.Translator = &AzureOpenAITranslator{}
3739

40+
// NewAzureOpenAITranslator initializes a new AzureOpenAITranslator and returns its pointer.
3841
func NewAzureOpenAITranslator() *AzureOpenAITranslator {
3942
return &AzureOpenAITranslator{
4043
apiVersion: defaultAPIVersion,

payload-processing/pkg/plugins/api-translation/translator/translator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2026 The opendatahub.io Authors.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.

payload-processing/pkg/plugins/api-translation/translator/vertex/vertex.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@ import (
2222
"regexp"
2323
"strings"
2424
"time"
25+
26+
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/api-translation/translator"
2527
)
2628

2729
const (
2830
vertexV1BetaPathTemplate = "/v1beta/models/%s:generateContent"
2931
)
3032

3133
// compile-time interface check
32-
// var _ translator.Translator = &VertexTranslator{}
34+
var _ translator.Translator = &VertexTranslator{}
3335

36+
// NewVertexTranslator initializes a new VertexTranslator and returns its pointer.
3437
func NewVertexTranslator() *VertexTranslator {
3538
return &VertexTranslator{
3639
modelNamePattern: regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`), // modelNamePattern validates Gemini model names to prevent path injection.

payload-processing/pkg/plugins/common/provider/provider.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2025.
2+
Copyright 2026.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)