Skip to content

Commit a0e4ade

Browse files
committed
feat: declare ResponseBodyMode on api-translation and nemo-response-guard
- api-translation: configurable via "responseBodyMode" parameter (none/chunked/full, defaults to full) - nemo-response-guard: always BodyFull (needs complete body for content inspection) Depends on opendatahub-io#331 (framework migration)
1 parent 81de570 commit a0e4ade

6 files changed

Lines changed: 51 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ bin/*
33

44
# Downloaded CRDs for envtest (regenerated by hack/download-test-crds.sh)
55
test/testdata/crds/
6+
.claude/

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ require (
1313
k8s.io/kubectl v0.35.5
1414
sigs.k8s.io/controller-runtime v0.23.3
1515
sigs.k8s.io/gateway-api v1.5.1
16+
sigs.k8s.io/gateway-api-inference-extension v1.5.0
1617
)
1718

1819
require (
1920
github.com/Masterminds/semver/v3 v3.4.0 // indirect
2021
github.com/aws/smithy-go v1.25.1 // indirect
2122
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
22-
github.com/goccy/go-yaml v1.19.2 // indirect
2323
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
2424
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
2525
github.com/moby/spdystream v0.5.1 // indirect
@@ -125,4 +125,4 @@ require (
125125
sigs.k8s.io/yaml v1.6.0 // indirect
126126
)
127127

128-
replace github.com/llm-d/llm-d-inference-payload-processor => ../llm-d-inference-payload-processor
128+
replace github.com/llm-d/llm-d-inference-payload-processor => github.com/noyitz/llm-d-inference-payload-processor v0.0.0-20260616213505-bf93834c3cf2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
143143
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
144144
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
145145
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
146+
github.com/noyitz/llm-d-inference-payload-processor v0.0.0-20260616213505-bf93834c3cf2 h1:4ih3EdzAhpcA70oELfGv0qx5EMYR8RHPgF2Bgl4j1jA=
147+
github.com/noyitz/llm-d-inference-payload-processor v0.0.0-20260616213505-bf93834c3cf2/go.mod h1:UZldktV/E/LtaFHrtZdPOhy1d7WdazMRe3Q9OW7f9jI=
146148
github.com/onsi/ginkgo/v2 v2.30.0 h1:zxM/9XneXFIy64j6/wAmBIX4zRC7Hu6U8XFNZvDnCQc=
147149
github.com/onsi/ginkgo/v2 v2.30.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
148150
github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA=

pkg/plugins/api-translation/plugin.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ const (
4747
// compile-time type validation
4848
var _ requesthandling.RequestProcessor = &APITranslationPlugin{}
4949
var _ requesthandling.ResponseProcessor = &APITranslationPlugin{}
50+
var _ requesthandling.ResponseBodyRequirement = &APITranslationPlugin{}
5051

5152
// apiTranslationConfig holds configuration for provider-specific translators.
5253
type apiTranslationConfig struct {
53-
VertexOpenAI *vertexOpenAIConfig `json:"vertexOpenAI,omitempty"`
54+
VertexOpenAI *vertexOpenAIConfig `json:"vertexOpenAI,omitempty"`
55+
BodyProcessingMode string `json:"responseBodyMode,omitempty"`
5456
}
5557

5658
type vertexOpenAIConfig struct {
@@ -100,27 +102,34 @@ func NewAPITranslationPlugin(ctx context.Context, config apiTranslationConfig) (
100102
)
101103
}
102104

105+
bodyMode, err := parseBodyProcessingMode(config.BodyProcessingMode)
106+
if err != nil {
107+
return nil, err
108+
}
109+
103110
keys := make([]string, 0, len(providers))
104111
for key := range providers {
105112
keys = append(keys, key)
106113
}
107114

108-
log.FromContext(ctx).V(logutil.VERBOSE).Info("plugin initialized", "providers", strings.Join(keys, ","))
115+
log.FromContext(ctx).V(logutil.VERBOSE).Info("plugin initialized", "providers", strings.Join(keys, ","), "responseBodyMode", bodyMode)
109116

110117
return &APITranslationPlugin{
111118
typedName: plugin.TypedName{
112119
Type: APITranslationPluginType,
113120
Name: APITranslationPluginType,
114121
},
115-
providers: providers,
122+
providers: providers,
123+
responseBodyMode: bodyMode,
116124
}, nil
117125
}
118126

119127
// APITranslationPlugin translates inference API requests and responses between
120128
// OpenAI Chat Completions format and provider-native formats (e.g., Anthropic Messages API).
121129
type APITranslationPlugin struct {
122-
typedName plugin.TypedName
123-
providers map[string]translator.Translator // map from provider name to translator interface
130+
typedName plugin.TypedName
131+
providers map[string]translator.Translator // map from provider name to translator interface
132+
responseBodyMode requesthandling.BodyProcessingMode
124133
}
125134

126135
// TypedName returns the type and name tuple of this plugin instance.
@@ -134,6 +143,25 @@ func (p *APITranslationPlugin) WithName(name string) *APITranslationPlugin {
134143
return p
135144
}
136145

146+
// BodyProcessingMode returns the configured response body mode.
147+
// Defaults to Full. Configurable via the "responseBodyMode" parameter.
148+
func (p *APITranslationPlugin) BodyProcessingMode() requesthandling.BodyProcessingMode {
149+
return p.responseBodyMode
150+
}
151+
152+
func parseBodyProcessingMode(s string) (requesthandling.BodyProcessingMode, error) {
153+
switch strings.ToLower(s) {
154+
case "", "full":
155+
return requesthandling.Full, nil
156+
case "chunked":
157+
return requesthandling.Chunks, nil
158+
case "none":
159+
return requesthandling.Skip, nil
160+
default:
161+
return 0, fmt.Errorf("invalid responseBodyMode %q (valid: none, chunked, full)", s)
162+
}
163+
}
164+
137165
// ProcessRequest reads the provider from CycleState (set by an upstream plugin) and translates
138166
// the request body from OpenAI format to the provider's native format if needed.
139167
// When the incoming client format matches the upstream API format (passthrough mode),

pkg/plugins/apikey-injection/auth-generator/simple_auth_generator_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ package authgenerator
1919
import (
2020
"testing"
2121

22+
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/plugin"
23+
"github.com/llm-d/llm-d-inference-payload-processor/pkg/framework/interface/requesthandling"
24+
2225
"github.com/google/go-cmp/cmp"
2326
"github.com/google/go-cmp/cmp/cmpopts"
2427
"github.com/stretchr/testify/require"
25-
"sigs.k8s.io/gateway-api-inference-extension/pkg/bbr/framework"
2628

2729
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/common/auth"
2830
"github.com/opendatahub-io/ai-gateway-payload-processing/pkg/plugins/common/state"
@@ -154,11 +156,11 @@ func TestSimpleExtractRequestData(t *testing.T) {
154156

155157
for _, test := range tests {
156158
t.Run(test.name, func(t *testing.T) {
157-
cs := framework.NewCycleState()
159+
cs := plugin.NewCycleState()
158160
cs.Write(state.ModelConfigKey, test.config)
159161

160162
generator := NewSimpleAuthGenerator()
161-
result, err := generator.ExtractRequestData(cs, framework.NewInferenceRequest())
163+
result, err := generator.ExtractRequestData(cs, requesthandling.NewInferenceRequest())
162164

163165
require.NoError(t, err)
164166
require.Equal(t, test.wantHeaderName, result[auth.SimpleAuthHeaderName])
@@ -169,7 +171,7 @@ func TestSimpleExtractRequestData(t *testing.T) {
169171

170172
func TestSimpleExtractRequestData_MissingModelConfigKey(t *testing.T) {
171173
generator := NewSimpleAuthGenerator()
172-
_, err := generator.ExtractRequestData(framework.NewCycleState(), framework.NewInferenceRequest())
174+
_, err := generator.ExtractRequestData(plugin.NewCycleState(), requesthandling.NewInferenceRequest())
173175
require.Error(t, err)
174176
require.Contains(t, err.Error(), "failed to extract config from cycle state")
175177
}

pkg/plugins/nemo/response_guard.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const (
3333

3434
// compile-time type validation
3535
var _ requesthandling.ResponseProcessor = &NemoResponseGuardPlugin{}
36+
var _ requesthandling.ResponseBodyRequirement = &NemoResponseGuardPlugin{}
3637

3738
// NemoResponseGuardPlugin calls a NeMo Guardrails service over HTTP to check model output
3839
// using output rails. It implements ResponseProcessor to inspect responses before returning
@@ -85,6 +86,12 @@ func (p *NemoResponseGuardPlugin) WithName(name string) *NemoResponseGuardPlugin
8586
return p
8687
}
8788

89+
// BodyProcessingMode returns Full because NeMo guardrails need the complete response
90+
// body to inspect the full model output.
91+
func (p *NemoResponseGuardPlugin) BodyProcessingMode() requesthandling.BodyProcessingMode {
92+
return requesthandling.Full
93+
}
94+
8895
// ProcessResponse calls NeMo Guardrails to evaluate output rails on the model response.
8996
// It extracts assistant messages from the OpenAI-style response body, POSTs them to the
9097
// configured NeMo endpoint, and returns an errcommon.Error with Forbidden (403) if NeMo

0 commit comments

Comments
 (0)