Skip to content

[FEATURE]: Plugins can't intercept outbound requests for AI-SDK models (http.request hook excludes the AI-SDK path) #42846

Closed

Description

@BenSharir

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

The runtime hook ctx.session.hook("http.request", (event) => { … event.request … }) lets a plugin mutate the outbound Web Request immediately before provider dispatch. Per the docs, though, it applies to native models only — AI-SDK-routed models don't pass through it:

From https://opencode.ai/v2/docs/build/plugins:
"HTTP hooks can modify requests and responses. They apply to native models; AI SDK models do not currently pass through these hooks. Request and response bodies are one-shot streams."

The separate ctx.aisdk.hook("sdk" | "language", …) capability covers the AI-SDK seam but exposes only the SDK/model objects, not the outbound HTTP request/body. So for any provider configured with a custom OpenAI-compatible baseURL (self-hosted proxies, gateways like a LiteLLM/router layer, local model servers) — all of which route through the AI SDK — a plugin currently cannot inspect or rewrite the outbound request at all.

Why it matters

This is the same motivation as #19425 ("pre/post API call hooks … for secret redaction and exfiltration prevention"), but for the AI-SDK path that the shipped hook skips:

  1. Security / governance (primary). Redact secrets from outbound bodies, enforce egress policy, and audit/log exactly what leaves the machine — for self-hosted-proxy and gateway deployments, which are common in orgs that route everything through one controlled endpoint. Today none of this is possible for AI-SDK providers.

  2. Payload shaping. Normalize or strip fields, inject required headers/tenant tags per baseURL, etc.

  3. Request-body compression (concrete, broadly useful). Large, growing JSON contexts are re-uploaded every turn. Compressing the body + setting Content-Encoding is a real, standardized optimization that multiple real targets already accept:

    Being explicit so this doesn't overclaim: OpenAI does not support request-body compression (confirmed in that same LiteLLM thread), and Anthropic/Azure/Bedrock don't document it either. The point is not "all providers" — it's that a plugin should be able to compress where the target accepts it (Vertex, Cerebras, self-hosted proxies), scoped by baseURL, and leave the rest untouched. On a constrained uplink a ~600 KB (≈150K-token) body is tens of seconds of pure upload wait per round-trip; JSON compresses ~5–8×.

All three use cases require the one missing capability: a plugin-reachable seam that can rewrite the outbound request (headers + body) on the AI-SDK path, scoped by target.

Ask

Let plugins intercept/rewrite the outbound request for AI-SDK-routed models — either by extending http.request/http.response dispatch to cover the AI-SDK path, or by adding an outbound-request seam to ctx.aisdk.hook (whichever fits the architecture). The event should expose the request (headers + body, replaceable) and enough model/provider/baseURL context that a plugin can scope changes to a specific endpoint and leave all other providers untouched. If this is intentionally out of scope, documenting the supported alternative would also close the gap.

Happy to test a fix end-to-end against a live Content-Encoding: zstd/gzip-accepting endpoint.

Related

Narrows #19425 and #2661 (generic pre-provider / body-rewrite hook requests) to the now-actionable gap: the hook shipped, but the AI-SDK dispatch path is excluded.

Activity

  1. github-actions commented on Aug 16, 2026

    @github-actions
    Contributor

    This issue doesn't fully meet our contributing guidelines.

    What needs to be fixed:

    • This issue does not use one of the required issue templates (Bug Report, Feature Request, or Question). Feature requests should use the Feature Request template and have a title starting with [FEATURE]:.

    Please edit this issue to address the above within 2 hours, or it will be automatically closed.

    If you believe this was flagged incorrectly, please let a maintainer know.

  2. changed the title [-]Plugins can't intercept outbound requests for AI-SDK models (http.request hook excludes the AI-SDK path)[/-] [+][FEATURE]: Plugins can't intercept outbound requests for AI-SDK models (http.request hook excludes the AI-SDK path)[/+] on Aug 16, 2026
  3. github-actions commented on Aug 16, 2026

    @github-actions
    Contributor

    Thanks for updating the issue! It now meets the contributing guidelines. We appreciate the detailed write-up.

  4. tespio commented on Oct 12, 2026

    @tespio

    Concrete instance of this gap, with a working implementation — because it is the same missing seam.

    The case

    A provider whose endpoint is reachable only through a corporate HTTP proxy, while every other
    provider connects directly:

    workstation --VPN--> [ proxy 192.168.10.101:3128 ] --LAN--> [ vLLM 192.168.10.86:5005 ]
            \------------------> public internet: reachable directly
    
    Probe Result
    TCP connect to 192.168.10.86:5005 directly connect timeout — no route
    TCP connect to 192.168.10.101:3128 reachable
    CONNECT 192.168.10.86:5005 via the proxy HTTP/1.1 200 Connection established

    The direct route is absent, not misconfigured. Exactly one destination is gated, so routing must be
    scoped to one provider — which is the "scoped by target" requirement in this issue, not a separate
    feature request.

    Why the existing options do not cover it

    HTTP_PROXY works and is process-wide, so it applies to traffic that must not be proxied. The
    consequence is already tracked: #49796 is an open, reproduced issue where a process-wide proxy breaks
    OpenCode's own local client, because it dials http://0.0.0.0:<port> and the CLI does not rewrite
    that to loopback. A seam scoped by target cannot produce that failure. NO_PROXY can express "one
    destination", but not two providers behind two different proxies, and it applies to every tool in the
    process rather than to a request.

    There is currently no hook that can express this, which is the claim of this issue. What exists
    is a workaround, and it is worth reporting what it took, because all four points are properties of
    the AI-SDK path rather than of the plugin:

    What the workaround had to do

    opencode-fetch-proxy (npm, MIT) wraps the global fetch that AI SDK providers fall back to, and
    routes per provider. It works in daily use against the topology above, including SSE streaming. Four
    findings:

    1. undici's ProxyAgent is not reliably importable from where a plugin runs — in the desktop
      sidecar it does not resolve, and import { ProxyAgent } from "undici" then degrades to a direct
      connection. That is a silent failure: it presents as the proxy being unreachable. A hand-rolled
      CONNECT tunnel over node:net/node:tls (TLS negotiated inside the tunnel) is what actually
      carries traffic. A native seam should choose its transport by runtime detection, not assumption.
    2. Loopback must be refused unconditionally. A provider pointed at 127.0.0.1 with a proxy
      configured fails with 503 Service Unavailable, because the proxy has no route back to the
      workstation's loopback — the same class as cli: service configured with hostname 0.0.0.0 is unreachable when an HTTP proxy is set #49796, and it surfaces as a hang rather than an error.
    3. Model discovery is a separate request. GET /v1/models must be routed too, or the provider
      fails before the first completion. Any hook that only covers /chat/completions is insufficient.
    4. The global fetch patch is process-wide, so the workaround reproduces the scope problem it
      exists to solve, one level down. That is the strongest argument for the seam this issue asks for.

    The ask, restated from this use case

    An outbound-request seam on the AI-SDK path that exposes enough provider/baseURL context for a
    plugin to scope changes to one endpoint, with the ability to substitute the transport (not only
    mutate headers/body). Header/body rewriting alone would not cover proxy routing, because the
    connection itself has to go somewhere else.

    Happy to test a fix end-to-end against a live corporate proxy and a live LAN target — the topology is
    real and reproducible, and the workaround gives a reference implementation to compare against.

  5. tespio commented on Oct 12, 2026

    @tespio

    Correction to the above: I described the implementation without linking it.

    The code referenced above is in index.js at that repo. Pointers to the four findings, for anyone
    reading this later:

    It is a workaround, not a proposed design, and the fourth finding above is the reason: a global
    fetch patch cannot scope itself, which is what this issue asks to fix properly.

  6. opencode-agent commented on Oct 13, 2026

    @opencode-agent
    Contributor

    This is fixed on dev: #50487 made the session http.request/http.response hooks run on the AI SDK route too, and custom OpenAI-compatible providers (@ai-sdk/openai-compatible) now use the native route, where these hooks already applied. Plugins can inspect or replace outbound headers and bodies (redaction, compression, etc.) for both, scoped by providerID or the request URL.

  7. tespio commented on Oct 14, 2026

    @tespio

    Thanks — #50487 is the right fix for the header/body case, and routing custom
    @ai-sdk/openai-compatible providers through the native path is a real improvement.

    One question, so I know whether the proxy case is covered or out of scope.

    The note describes the seam as "inspect or replace outbound headers and bodies", with redaction and
    compression as the examples. Both are transformations of a request that is already going to the same
    place. A proxy changes where the connection goes — it is decided before any header exists, and for
    https the TLS session is established inside the tunnel to the proxy, not to the origin.

    Concretely, for this case the provider must be dialled through CONNECT 192.168.10.86:5005 at
    192.168.10.101:3128 rather than directly. Nothing in the request changes; the socket does.

    So: does the new seam allow substituting the transport/fetch (connection level), or only rewriting
    the request on the SDK's own fetch?
    If it is the former, this issue is genuinely closed and I will
    retire the workaround. If it is the latter, the gap is narrower than when I filed this and worth
    tracking separately as scope — not as a re-open.

    The reason I ask rather than assume: undici's ProxyAgent does not resolve where plugins run in
    the desktop sidecar, and it fails by silently connecting directly, which presents as the proxy
    being unreachable. Anything built on this seam for the proxy case needs the transport substitution to
    be explicit, or that failure mode comes back.

    Either way, happy to test on dev against the real topology and report back — a working reference
    implementation is linked in my earlier comment.

  8. rekram1-node commented on Oct 14, 2026

    @rekram1-node
    Collaborator

    Yep, you can actually do this with session.http.request and session.http.response without patching globalThis.fetch.

    Since both event.request and event.response are replaceable Web Request / Response objects, you have full control over what gets sent and what response stream gets handed back.

    Here's a basic example of how you could run your CONNECT tunnel in-memory without opening a local port, by turning the default fetch into a no-op data: request and swapping in your tunnelled response:

    const original = new WeakMap<Request, Request>()
    
    ctx.session.hook(
      "http.request",
      async (event) => {
        const noop = new Request("data:text/plain,")
        original.set(noop, event.request)
        event.request = noop
      },
      { providerID: "my-vllm" },
    )
    
    ctx.session.hook(
      "http.response",
      async (event) => {
        const req = original.get(event.request)
        if (!req) return
        event.response = await tunnelledFetch(req, proxyUrl)
      },
      { providerID: "my-vllm" },
    )
    Or if you prefer a single hook, you could rewrite event.request in http.request (or event.baseURL in model.request) to point at a local 127.0.0.1 forwarder in your plugin that tunnels to the proxy.
    That's just a rough sketch though. You can probably have your agent look at packages/core/src/session/model-request.ts, packages/core/src/aisdk.ts, and https://opencode.ai/v2/docs/build/plugins to figure out what's best for your setup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions