Skip to content

feat(adapters): request-rewriting interceptors#4

Closed
Glorf wants to merge 2 commits into
feat/adapter-basefrom
feat/adapter-rewrites
Closed

feat(adapters): request-rewriting interceptors#4
Glorf wants to merge 2 commits into
feat/adapter-basefrom
feat/adapter-rewrites

Conversation

@Glorf

@Glorf Glorf commented Jun 3, 2026

Copy link
Copy Markdown
Owner

PR 4 of 4 — request-rewriting family. Built on feat/adapter-base (independent of the observability and caching PRs).

Adds 6 interceptors that mutate the outbound request body before it reaches the upstream:

  • drop_params remove named parameters from outbound body
  • payload_modifier add / remove / rename body fields
  • system_message inject a system message (prepend / append /
    replace)
  • consolidate_system merge displaced system messages into one at
    position 0 (Qwen3 requires this; other models
    tolerate it)
  • modify_tools strip or add properties on
    tools[].function.parameters
  • turn_counter per-session turn budget; raises GracefulError
    (-> 429) on exhaustion; injects warn/urgent
    system messages at 80%/95% of budget

All six are registered as builtins in InterceptorRegistry._BUILTIN.

Tests

test_adapter_interceptors.py drop_params, modify_tools,
system_message (prepend / replace),
payload_modifier (add / remove),
turn_counter (basic + session
isolation)
test_adapter_consolidate_system.py displaced-system-message merging
edge cases
test_adapter_parity_replay.py 6 captured-fixture scenarios

81 tests pass (42 from feat/adapter-base + 39 added).

Docs

fern/.../model-server/adapters.mdx appends Request-Rewriting section
with per-interceptor YAML config
snippets.

Glorf added 2 commits June 3, 2026 13:53
Introduces an interceptor-based middleware framework that runs at every
in-tree server's request/response boundary. PR 1 of 4 — base framework
only. Follow-on PRs add observability / caching / request-rewriting
interceptor families.

Framework

  nemo_gym/adapters/
    pipeline.py      AdapterPipeline + stage-order validation
                     (REQUEST → REQUEST_TO_RESPONSE → RESPONSE)
    middleware.py    install_middleware(app, specs) — FastAPI middleware
                     that wraps call_next; body replay, multi-Set-Cookie
                     preservation, /s/<hex>/... session-prefix routing,
                     GracefulError → 429
    proxy.py         start_adapter_proxy(upstream_url, adapters) —
                     localhost uvicorn host mode for external-inference
                     agents whose SDK respects *_BASE_URL env vars
    registry.py      short-name → Interceptor class + runtime register()
    types.py         AdapterRequest/Response, three Interceptor ABCs,
                     Stage enum, GracefulError, ContextVar-backed
                     per-request context, InterceptorSpec and
                     AdapterProxyConfig pydantic models
    interceptors/
      endpoint.py    Drives upstream HTTP call (required by proxy mode,
                     forbidden inside install_middleware)
      request_logging.py
                     Canonical "did the chain fire?" probe (logs body
                     keys + response status/latency)

Server wire-up

  adapters: list[dict] | None = None  on three base configs:
    BaseResponsesAPIModelConfig
    BaseResponsesAPIAgentConfig
    BaseResourcesServerConfig

  install_middleware(app, self.config.adapters)  at the tail of each
  base's setup_webserver. Every in-tree server inheriting from
  SimpleResponsesAPI{Model,Agent} or SimpleResourcesServer picks up
  the adapters knob automatically.

  adapter_proxy: AdapterProxyConfig | None = None  on
  BaseResponsesAPIAgentConfig. When set, SimpleResponsesAPIAgent.
  setup_webserver starts a localhost uvicorn proxy in a daemon thread,
  stores the ProxyHandle on self._proxy_handle, registers atexit cleanup.

Per-server override fixes

  harbor_agent and mini_swe_agent override setup_webserver without
  calling super(). Inline install_middleware(app, self.config.adapters)
  calls added to both so the adapters field still applies.

claude_code_agent integration

  Reads self._proxy_handle.url when adapter_proxy is set; threads the
  proxy URL into the claude CLI subprocess via ANTHROPIC_BASE_URL +
  ANTHROPIC_AUTH_TOKEN. Preserves the full model-name prefix in proxy
  mode (no .split("/")[-1] stripping).

Safety

  - Stage ordering validated at startup
  - Unknown interceptor name raises at config-validation time
  - install_middleware rejects `endpoint` in chain (host already forwards)
  - start_adapter_proxy rejects user-supplied `endpoint` AND refuses
    host="0.0.0.0" unless unsafe_allow_remote=True (otherwise leaks
    the upstream API key to any caller on the network)
  - best_effort=True interceptors swallow exceptions; strict ones
    propagate

Tests

  42 tests, framework-level coverage. Follow-on PRs add per-interceptor
  test suites.

Docs

  fern/versions/latest/pages/model-server/adapters.mdx       new
  fern/versions/latest/pages/model-server/index.mdx          link added

Signed-off-by: Michal Bien <mbien@nvidia.com>
PR 4 of 4 — request-rewriting family. Built on feat/adapter-base
(independent of the observability and caching PRs).

Adds 6 interceptors that mutate the outbound request body before it
reaches the upstream:

  - drop_params         remove named parameters from outbound body
  - payload_modifier    add / remove / rename body fields
  - system_message      inject a system message (prepend / append /
                        replace)
  - consolidate_system  merge displaced system messages into one at
                        position 0 (Qwen3 requires this; other models
                        tolerate it)
  - modify_tools        strip or add properties on
                        tools[].function.parameters
  - turn_counter        per-session turn budget; raises GracefulError
                        (-> 429) on exhaustion; injects warn/urgent
                        system messages at 80%/95% of budget

All six are registered as builtins in InterceptorRegistry._BUILTIN.

Tests

  test_adapter_interceptors.py       drop_params, modify_tools,
                                     system_message (prepend / replace),
                                     payload_modifier (add / remove),
                                     turn_counter (basic + session
                                     isolation)
  test_adapter_consolidate_system.py displaced-system-message merging
                                     edge cases
  test_adapter_parity_replay.py      6 captured-fixture scenarios

81 tests pass (42 from feat/adapter-base + 39 added).

Docs

  fern/.../model-server/adapters.mdx  appends Request-Rewriting section
                                      with per-interceptor YAML config
                                      snippets.

Signed-off-by: Michal Bien <mbien@nvidia.com>
@Glorf

Glorf commented Jun 22, 2026

Copy link
Copy Markdown
Owner Author

Superseded by NVIDIA-NeMo#1651 — moved upstream and rebased clean onto upstream feat/adapter-base. Fork discontinued.

@Glorf Glorf closed this Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant