Skip to content

[Bug]: Bedrock streaming bypasses req_http_options plugins, preventing request body customization #696

@sonic182

Description

@sonic182

Provider (if applicable)

aws_bedrock

Model (if applicable)

amazon.nova-2-lite-v1:0

Bug Description

req_http_options: [plugins: [...]] can be used to customize outgoing request bodies for non-streaming generation, because those provider paths go through Req.

Amazon Bedrock streaming does not go through Req. ReqLLM.Providers.AmazonBedrock.attach_stream/4 builds a Finch.Request directly:

  1. Builds the body with formatter.format_request(model_id, context, translated_opts).
  2. Encodes it with Jason.encode!.
  3. Calls Finch.build(:post, url, headers, json_body).
  4. Signs the resulting Finch request.

Because this path bypasses Req, request plugins passed through req_http_options never run. This makes request body customization inconsistent between generate_text/3 and stream_text/3.

This matters for applications that need to inject provider-specific fields that are not modeled by ReqLLM yet, such as Bedrock model-specific generation/thinking configuration, routing hints, gateway compatibility fields, or temporary provider extensions.

Reproduction Code

defmodule ExtraBodyPlugin do
  def attach(%Req.Request{} = req, opts) do
    extra = Keyword.fetch!(opts, :extra)
    Req.Request.prepend_request_steps(req, extra_body: &inject(&1, extra))
  end

  defp inject(%Req.Request{body: body} = req, extra) when is_binary(body) do
    decoded = Jason.decode!(body)
    %{req | body: Jason.encode!(Map.merge(decoded, extra))}
  end

  defp inject(req, _extra), do: req
end

extra_body = %{
  "generationConfig" => %{
    "thinkingConfig" => %{"thinkingLevel" => "medium"}
  }
}

opts = [
  req_http_options: [
    plugins: [
      fn req -> ExtraBodyPlugin.attach(req, extra: extra_body) end
    ]
  ]
]

# Non-streaming paths that use Req can run the plugin.
ReqLLM.Generation.generate_text(
  %{provider: :amazon_bedrock, id: "model-id", provider_model_id: "provider-model-id"},
  "hello",
  opts
)

# Bedrock streaming bypasses Req and builds a Finch.Request directly,
# so the plugin never runs and the extra body is not injected.
ReqLLM.Generation.stream_text(
  %{provider: :amazon_bedrock, id: "model-id", provider_model_id: "provider-model-id"},
  "hello",
  opts
)


The relevant Bedrock streaming code path is:


body = formatter.format_request(model_id, context, translated_opts)
json_body = body |> ReqLLM.Schema.apply_property_ordering() |> Jason.encode!()
finch_request = Finch.build(:post, url, headers, json_body)

Expected Behavior

A supported request body customization mechanism should work consistently for both:

  • ReqLLM.Generation.generate_text/3
  • ReqLLM.Generation.stream_text/3

Callers should not need separate provider-specific workarounds depending on whether streaming is enabled.

Actual Behavior

For Bedrock non-streaming, request customization can work because the request path uses Req.

For Bedrock streaming, customization does not apply because attach_stream/4 builds and signs a Finch.Request directly. req_http_options headers can still be extracted, but Req plugins never run and cannot modify the body.

Environment

  • ReqLLM: 1.10.0
  • Elixir: 1.19.5-otp-28
  • Erlang/OTP: 28.4.2
  • Provider: :amazon_bedrock
  • Streaming: ReqLLM.Generation.stream_text/3

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions