Skip to content

aws: Bedrock 4xx errors such as ValidationException are retried 3 times, so a bad request takes ~7 s to fail #7417

Description

@kimnamu

Bug Description

Thanks for the retry logic in LLMStream and the 4xx rule in APIStatusError. The AWS plugin doesn't use that rule yet, so Bedrock's permanent 4xx errors get retried like connection errors.

LLMStream._run in the AWS plugin wraps every exception raised before the first chunk in APIConnectionError(retryable=True) (llm.py L343-L347). A ValidationException from converse_stream() therefore goes out 4 times and takes about 7 s to reach the caller, though it fails the same way every time. The APIStatusError(retryable=False) the plugin raises for a non-200 response (L327-L335) is caught by the same except and re-wrapped as retryable too.

I hit this while testing OpenAI GPT-6 on Bedrock, where a few settings carried over from gpt-oss are rejected:

request, us-east-1, main (b4df92e) HTTP requests sent time to error
us.openai.gpt-6-sol + additional_request_fields={"reasoning_effort": "high"} 4 7.33 s
us.openai.gpt-6-sol + max_output_tokens=200000 4 7.06 s
us.openai.gpt-6-luna, same two requests 4 / 4 7.02 s / 7.35 s
plain request (control) 1 1.20 s, pong

Expected Behavior

Bedrock's own errors keep their HTTP status, as they do with the anthropic plugin for the Anthropic SDK's status errors (anthropic/llm.py L341-L347). Then APIStatusError's existing rule applies: 408/429/5xx are retried and other 4xx fail on the first attempt.

Reproduction Steps

cat > repro.py <<'PY'
import asyncio, time
from livekit.agents.llm import ChatContext
from livekit.plugins import aws

async def main():
    llm = aws.LLM(model="us.openai.gpt-6-sol", max_output_tokens=200000)
    sent = 0
    def count(**_):
        nonlocal sent
        sent += 1
    llm._session.register("before-send.bedrock-runtime.ConverseStream", count)
    ctx = ChatContext()
    ctx.add_message(role="user", content="Say pong.")
    t0 = time.perf_counter()
    try:
        await llm.chat(chat_ctx=ctx).collect()
    except Exception as e:
        print(f"requests={sent} {time.perf_counter() - t0:.2f}s {type(e).__name__}")

asyncio.run(main())
PY
uv run python repro.py
requests=4 7.06s APIConnectionError

Operating System

macOS 26 (not OS-specific)

Models Used

AWS Bedrock via aws.LLM (any model; GPT-6 Sol/Luna above)

Package Versions

livekit-agents==1.8.2
livekit-plugins-aws==1.8.2   # main at b4df92e
aiobotocore==3.8.0
botocore==1.43.46
python==3.13

Session/Room/Call IDs

N/A, reproduces with aws.LLM alone.

Proposed Solution

                try:
                    response = await client.converse_stream(**self._opts)
                except ClientError as e:
                    meta = e.response.get("ResponseMetadata", {})
                    raise APIStatusError(
                        f"aws bedrock llm: error generating content: {e}",
                        status_code=meta.get("HTTPStatusCode", -1),
                        request_id=meta.get("RequestId"),
                    ) from e
        ...
        except APIStatusError:
            raise
        except Exception as e:  # unchanged

With this patch the same requests fail after 1 request (0.65-1.08 s over four runs). I have it with a hermetic test and can open a PR.

Additional Context

What changes per error code

From the ConverseStream error shapes in botocore's bedrock-runtime model, sorted by APIStatusError's rule:

Bedrock error HTTP retried today retried with the patch
ValidationException 400 yes no
AccessDeniedException 403 yes no
ResourceNotFoundException 404 yes no
ModelErrorException 424 yes no
ModelTimeoutException 408 yes yes
ThrottlingException, ModelNotReadyException 429 yes yes
InternalServerException, ServiceUnavailableException 500, 503 yes yes

The 404 row is a judgment call: on GPT-6's launch day, 1 of 9 test calls got a transient ResourceNotFoundException: Inference Profile ARN not found. If you would rather keep 404/424 retryable, those codes can stay on the APIConnectionError path.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    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