Skip to content

[Feature]: Response body-transform extension point (symmetric to #255) for post-dispatch payload rewriting #259

Description

@shimib

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, responseBodyResultMessage.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions