Problem Statement
The worker assumes one incoming response shape: whatever bytes HTTPInferenceClient.SendRequest returns are published verbatim as the result Payload (pkg/asyncworker/worker.go, responseBody → ResultMessage.Payload). Any provider that needs the response rewritten before it reaches the result queue — model-name overriding, field remapping, unwrapping a provider-specific envelope back into the OpenAI-style shape, multipart/binary → JSON normalization — currently has no extension point and would have to patch a provider-specific branch into the worker. That's the same scaling problem #255 solved for the request side, on the response side.
This is the symmetric counterpart to the request body-transform extension point (#255, implemented in #257). Raised by @jtechapps in review of #257: "we should also support response body-transform. One use case: model overriding will have to do some sort of response transforming." It is intentionally out of scope for #257 and tracked here as its own framework.
Proposed Solution
Mirror the request-side design (#255) and reuse the framework already landed in #257 — the EPP-style pkg/plugins base (Plugin, TypedName, FactoryFunc, Register/Lookup, Handle) and the transform.Chain shape — so request and response transforms share identity, registry, config loading, and chaining conventions.
- Typed capability interface — a response-side contract the worker invokes after a successful dispatch:
// ResponseTransform is a post-dispatch body-transform plugin. The worker invokes
// the configured chain after SendRequest returns and before the result is
// published. A plugin that doesn't recognize a response returns handled=false,
// preserving the verbatim payload.
type ResponseTransform interface {
plugins.Plugin // TypedName() plugins.TypedName
// Transform optionally rewrites the response body. When it recognizes the
// response it returns the new body and handled=true; otherwise handled=false
// and the original responseBody is published unchanged.
Transform(responseBody []byte, metadata map[string]string) (body []byte, handled bool, err error)
}
- Factory + registry + config + chain — identical to the request side: register a
FactoryFunc under a plugin type, load an ordered chain from config, first-handled-wins/short-circuit, nil chain = no-op. Likely a sibling type in pkg/asyncworker/transform reusing pkg/plugins.
- Host integration point — in
pkg/asyncworker/worker.go, on the err == nil success path: run the response chain on responseBody before constructing ResultMessage{Payload: string(responseBody), ...}. A transform error on this path is a result-failure (surface via CreateErrorResultMessage), not a retry, since the upstream call already succeeded.
- Wiring — a
--response-transform-config-file flag (or a combined transform config), threaded to the worker alongside the request *transform.Chain. Empty config ⇒ no transforms ⇒ current byte-for-byte behavior.
Open Questions
- Shared vs separate config file / CLI flag — one transform config describing both directions, or two.
- Metadata availability — same opaque
ReqMetadata() pass-through the request side keys off; confirm that's sufficient for model-override use cases or whether response transforms need request payload context too.
- Interface symmetry — whether
ResponseTransform needs a Validate step (the request side uses it for deadline-aware preflight; the response side has no obvious analog) — likely omitted.
- Error semantics — confirm a failed response transform is non-retryable (upstream already consumed capacity).
Alternatives Considered
Willingness to Contribute
Yes, can submit a PR (after #257 lands, to build on its framework).
Additional Context
Problem Statement
The worker assumes one incoming response shape: whatever bytes
HTTPInferenceClient.SendRequestreturns are published verbatim as the resultPayload(pkg/asyncworker/worker.go,responseBody→ResultMessage.Payload). Any provider that needs the response rewritten before it reaches the result queue — model-name overriding, field remapping, unwrapping a provider-specific envelope back into the OpenAI-style shape, multipart/binary → JSON normalization — currently has no extension point and would have to patch a provider-specific branch into the worker. That's the same scaling problem #255 solved for the request side, on the response side.This is the symmetric counterpart to the request body-transform extension point (#255, implemented in #257). Raised by @jtechapps in review of #257: "we should also support response body-transform. One use case: model overriding will have to do some sort of response transforming." It is intentionally out of scope for #257 and tracked here as its own framework.
Proposed Solution
Mirror the request-side design (#255) and reuse the framework already landed in #257 — the EPP-style
pkg/pluginsbase (Plugin,TypedName,FactoryFunc,Register/Lookup,Handle) and thetransform.Chainshape — so request and response transforms share identity, registry, config loading, and chaining conventions.FactoryFuncunder a plugin type, load an ordered chain from config, first-handled-wins/short-circuit, nil chain = no-op. Likely a sibling type inpkg/asyncworker/transformreusingpkg/plugins.pkg/asyncworker/worker.go, on theerr == nilsuccess path: run the response chain onresponseBodybefore constructingResultMessage{Payload: string(responseBody), ...}. A transform error on this path is a result-failure (surface viaCreateErrorResultMessage), not a retry, since the upstream call already succeeded.--response-transform-config-fileflag (or a combined transform config), threaded to the worker alongside the request*transform.Chain. Empty config ⇒ no transforms ⇒ current byte-for-byte behavior.Open Questions
ReqMetadata()pass-through the request side keys off; confirm that's sufficient for model-override use cases or whether response transforms need request payload context too.ResponseTransformneeds aValidatestep (the request side uses it for deadline-aware preflight; the response side has no obvious analog) — likely omitted.Alternatives Considered
http_client.go— the special-casing [Feature]: Request body-transform extension point (EPP-style plugin framework) for dispatch-time payload rewriting #255 set out to remove; applies equally here.Willingness to Contribute
Yes, can submit a PR (after #257 lands, to build on its framework).
Additional Context
pkg/plugins+pkg/asyncworker/transformframework from feat: add request body-transform extension point #257.pipeline.GateFactory,WithGateFactory,cmd/main.go.