Skip to content

fix(bedrock): support bearer-token auth + raw image bytes for aux vision - #28085

Open
theodore3131 wants to merge 1 commit into
NousResearch:mainfrom
theodore3131:fix/bedrock-aux-vision
Open

fix(bedrock): support bearer-token auth + raw image bytes for aux vision#28085
theodore3131 wants to merge 1 commit into
NousResearch:mainfrom
theodore3131:fix/bedrock-aux-vision

Conversation

@theodore3131

@theodore3131 theodore3131 commented May 18, 2026

Copy link
Copy Markdown

fix(bedrock): support bearer-token auth + raw image bytes for aux vision

The auxiliary client could not service vision (or any other) tasks for
users who authenticated to Bedrock via AWS_BEARER_TOKEN_BEDROCK. Four
independent bugs combined to break this path end-to-end; all are fixed
here so a single PR restores the feature.

  1. auxiliary_client.py — bearer-token routing
    resolve_provider_client("bedrock", ...) unconditionally wrapped a
    freshly-built anthropic.AnthropicBedrock client in
    AnthropicAuxiliaryClient. AnthropicBedrock's auth helper only
    resolves IAM credentials from the boto3 credential chain and
    raises 'RuntimeError: could not resolve credentials from session'
    the moment a request is dispatched on bearer-token auth. The main
    model loop already handles this in
    hermes_cli.runtime_provider._build_runtime_provider by falling
    back to the boto3 Converse API for bearer tokens; the auxiliary
    client now mirrors that dual-path routing via a new
    BedrockConverseAuxiliaryClient (sync) and
    AsyncBedrockConverseAuxiliaryClient (async) that delegate to
    bedrock_adapter.call_converse.

  2. bedrock_adapter.py — image payload double-encoding
    convert_messages_to_converse forwarded the base64 string from a
    data: URL into image.source.bytes. That field is a Bedrock blob
    shape: boto3 expects raw bytes and base64-encodes the wire
    payload itself, so passing the string double-encoded the image
    and Bedrock returned 'ValidationException: Could not process
    image'. Decode the base64 once before assembling the block;
    gracefully skip malformed payloads.

  3. auxiliary_client.py — _resolve_task_provider_model priority
    The "base_url + api_key implies provider=custom" heuristic ran
    before the explicit-provider check. When the user (or
    hermes auxiliary) wrote

    auxiliary.vision:
        provider: bedrock
        base_url: https://bedrock-runtime.us-east-1.amazonaws.com
        api_key: <AWS_BEARER_TOKEN_BEDROCK>
    

    the provider directive was silently rewritten to "custom",
    handing the Bedrock URL + bearer token to the generic OpenAI
    HTTP client. Bedrock 200s on POST /chat/completions with an
    empty body, which surfaces downstream as
    'ChatCompletion(id=None, choices=None, ...)'. This trap also
    bites any other named provider whose adapter is incompatible
    with the OpenAI chat-completions wire (anthropic, nous,
    openai-codex, ...). Fix: explicit known provider wins over the
    heuristic, in both the early-args path and the task-config
    path.

  4. auxiliary_client.py — _is_unsupported_parameter_error markers
    Bedrock Converse + newer Anthropic models (Opus 4.7, Sonnet
    4.5+) reject temperature with the wording

    The model returned the following errors:
    `temperature` is deprecated for this model.
    

    The "deprecated" marker was missing from the detector, so the
    reactive-retry branch in call_llm never fired and the boto3
    ValidationException was swallowed by downstream wrappers,
    surfacing — once again — as an empty ChatCompletion object.
    Add "is deprecated" to the marker set so the existing
    strip-and-retry path engages automatically.

Tests

  • tests/agent/test_auxiliary_client_bedrock.py (new): verifies the
    routing split — bearer token picks BedrockConverseAuxiliaryClient,
    IAM credentials still pick AnthropicAuxiliaryClient — plus the
    adapter's call_converse delegation, max_completion_tokens fallback,
    stop-string normalization, and async wrapper identity for cache
    eviction.
  • tests/agent/test_bedrock_adapter.py: three new tests for the data
    URL → raw bytes conversion, JPEG format propagation, and graceful
    handling of malformed payloads.
  • tests/agent/test_bedrock_integration.py: existing
    TestAuxiliaryClientBedrockResolution suite gains an autouse
    _clean_aws_env fixture so user-level AWS_BEARER_TOKEN_BEDROCK no
    longer leaks in and routes the IAM-path tests through the wrong
    client.
  • tests/agent/test_resolve_task_provider_priority.py (new): locks
    in priority — explicit provider:bedrock with base_url+api_key
    must resolve to bedrock, not custom. Covers anthropic,
    openai-codex, plain custom-endpoint, auto, and the
    task-config path.
  • tests/agent/test_unsupported_parameter_retry.py: extended
    parametrize to cover Bedrock's "temperature is deprecated for
    this model" wording (full ValidationException string and the
    bare phrase).

End-to-end smoke: aux vision via Bedrock + bearer token now correctly
identifies a 64x64 red PNG via us.anthropic.claude-opus-4-7 (reply:
'Red', 34 prompt + 6 completion tokens) AND a real screenshot via
us.anthropic.claude-sonnet-4-6 (reply describing portfolio app
contents in full). Full agent test suite passes (3092 tests).

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/vision Vision analysis and image generation P3 Low — cosmetic, nice to have labels May 18, 2026
@theodore3131
theodore3131 force-pushed the fix/bedrock-aux-vision branch from 4c3a3bb to 192e79b Compare May 18, 2026 18:28

@outsourc-e outsourc-e left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran the touched Bedrock/auxiliary suites locally:

  • python3 -m pytest -q -o addopts='' tests/agent/test_auxiliary_client_bedrock.py tests/agent/test_bedrock_adapter.py tests/agent/test_bedrock_integration.py tests/agent/test_resolve_task_provider_priority.py tests/agent/test_unsupported_parameter_retry.py

Result: 209 passed.

I also checked the earlier async concern — the PR now has explicit async wrapper coverage for the converse client, so this looks merge-ready on the auxiliary/vision scope.

@theodore3131

Copy link
Copy Markdown
Author

Hi @teknium1 / @kshitijk4poor — this one's been approved by @outsourc-e but hasn't been merged yet. Would one of you be able to take a look when you have a chance?

Quick recap of impact: without this fix, Bedrock users who authenticate via AWS_BEARER_TOKEN_BEDROCK can't use auxiliary vision tasks at all (image-to-text, etc.) — the aux client routes Bedrock URLs through the OpenAI wire format and gets back empty completions.

Happy to rebase if needed. Thanks!

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused Bedrock aux/vision fix. I verified the underlying bugs still exist on current main: Bedrock aux routing always uses AnthropicAuxiliaryClient at agent/auxiliary_client.py:3938, data URL images still pass the base64 string into image.source.bytes at agent/bedrock_adapter.py:517, and _resolve_task_provider_model still forces base_url paths to custom before explicit providers at agent/auxiliary_client.py:4729.

Problems

  • The new bearer-token async path still looks broken. The PR returns _to_async_client(client, ...) for BedrockConverseAuxiliaryClient at agent/auxiliary_client.py:3600, but _to_async_client has no Bedrock branch on current main and falls through to AsyncOpenAI(api_key='aws-sdk', base_url=bedrock-runtime...) at agent/auxiliary_client.py:3288. That reintroduces the OpenAI-wire Bedrock mismatch for async auxiliary callers.
  • The malformed data URL handling is weaker than the test suggests. The PR uses base64.b64decode(data, validate=False) at agent/bedrock_adapter.py:486; Python accepts the test payload ====!!notvalid and returns bytes rather than raising, so malformed images are not reliably skipped.

Suggested changes

  • Add a _to_async_client branch returning AsyncBedrockConverseAuxiliaryClient(sync_client) and cover resolve_provider_client('bedrock', ..., async_mode=True) with AWS_BEARER_TOKEN_BEDROCK set.
  • Switch the data URL decode to strict validation and assert the malformed payload produces no image block.

Automated hermes-sweeper review; humans decide final merge/salvage.

Comment thread agent/auxiliary_client.py
"resolve_provider_client: bedrock converse (%s, %s, bearer-token)",
final_model, region,
)
return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still routes the new BedrockConverseAuxiliaryClient through _to_async_client(), but _to_async_client has no BedrockConverse branch and will fall through to AsyncOpenAI with the Bedrock runtime URL. Please add a branch returning AsyncBedrockConverseAuxiliaryClient(sync_client) and cover resolve_provider_client(..., async_mode=True) for the bearer-token path.

Comment thread agent/bedrock_adapter.py Outdated
# double-encodes the payload and the model returns
# ``ValidationException: Could not process image``.
try:
image_bytes = base64.b64decode(data, validate=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate=False does not reliably reject malformed data URLs; for example the new test payload ====!!notvalid decodes to bytes under Python's loose decoder. Use validate=True (and probably assert a non-empty decoded payload) if the intended behavior is to skip malformed images.

Comment thread tests/agent/test_bedrock_adapter.py Outdated
# truly cannot be decoded as base64 even loosely).
# An odd-length truncated string with a pad in the
# middle reliably raises binascii.Error.
"url": "data:image/png;base64,====!!notvalid",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This payload is not a reliable malformed-base64 fixture with validate=False: Python decodes it to bytes instead of raising, so the test can pass without exercising the skip path. Use a payload that fails strict validation and assert no image block is present.

@RamsAI-bot

Copy link
Copy Markdown

Verified this closes the auxiliary bearer-token gap from #29309, applied on top of #24507. The head (192e79b05) cherry-picks onto a stacked set of the bearer/Converse fixes with 0 conflicts — the one overlapping _convert_content_to_converse base64 hunk auto-merges to a single decode — and the 33 focused tests pass.

Result under bearer-only Bedrock: title generation and context compression now succeed, where previously every new session failed with could not resolve credentials from session.

One caveat worth flagging: auxiliary.vision was not fixed. With auxiliary.vision: {provider: bedrock, model: <claude>}, vision_analyze still returns an empty ChatCompletion(id=None, choices=None, ...). The tools/vision_tools.pyasync_call_llm path doesn't appear to route through the new BedrockConverseAuxiliaryClient / the explicit-provider-priority fix, so it still falls into the provider=custom empty-body trap. Title-gen + compression are unaffected. Happy to share a log excerpt if helpful.

#24507 + #28085 are the two PRs closest to a complete bearer-token Bedrock fix.

@RamsAI-bot

Copy link
Copy Markdown

Update — verified acd5de2da closes the vision gap I flagged above. On a bearer-token-only Bedrock deployment, with the new _to_async_client → AsyncBedrockConverseAuxiliaryClient branch, vision_analyze now returns real image descriptions (previously an empty ChatCompletion(id=None, choices=None, …)); title generation and compression are also working. So PR #28085 in full (both commits) now covers all three aux surfaces end-to-end under bearer-token auth.

acd5de2da lands exactly what @teknium1's 2026-06-15 review asked for (the missing _to_async_client branch + validate=True base64 validation). Thanks @theodore3131 — from our side it's verified working against real Bedrock, so this looks good to land.

@theodore3131

Copy link
Copy Markdown
Author

Friendly ping @teknium1 — this is now in a finished state and ready to land.

Your 2026-06-15 review asked for two things: the missing _to_async_client branch and validate=True base64 validation. Both shipped in acd5de2da. Independent verification on real bearer-token-only Bedrock (thanks @RamsAI-bot) confirms all three aux surfaces now work end-to-end:

  • title generation
  • context compression
  • vision_analyze ✅ — previously returned empty ChatCompletion(id=None, choices=None, …), now returns real image descriptions via the new AsyncBedrockConverseAuxiliaryClient branch.

No conflicts (MERGEABLE), already approved by @outsourc-e. Just needs a merge when you have a moment. Thanks!

@VictorPruefer

Copy link
Copy Markdown

@teknium1 Is there a chance to get this merged for the next update or do you have any other workarounds to get Anthropic models work on AWS Bedrock?

@theodore3131
theodore3131 force-pushed the fix/bedrock-aux-vision branch from acd5de2 to e184763 Compare July 13, 2026 07:17
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
@theodore3131

Copy link
Copy Markdown
Author

Review — rebase required; core aux-vision fix is still live and worth salvaging

Reviewed the 3-commit bedrock delta (d3fc357, 58a88f9, e18476). Code quality is high — fixes are at the shared boundary, well-commented with the exact failure modes, and the 284 tests across the 10 changed files all pass locally. But the branch is ~4,362 commits behind main and mergeable=false (dirty), so it needs a rebase before it can land. During rebase, note that main has moved:

Already landed on main independently — drop these on rebase

  • Main-loop bearer to Converse routing (resolve_runtime_provider): main now has the _has_bearer_token guard. This is fix(bedrock): correct api_mode detection for bearer-token auth #26531's commit — same logic, already merged.
  • Bedrock image raw-bytes (bedrock_adapter._convert_content_to_converse): main already does raw_bytes = base64.b64decode(data) then "source": {"bytes": raw_bytes}. Your validate=True + empty-payload guards are a nice hardening delta worth keeping as a small follow-up, but the core double-encode bug is fixed.

Still broken on main — this is the reason to keep this PR

  1. Aux-vision bearer-token pathresolve_provider_client's aws_sdk branch on main still does NOT split bearer vs IAM. On a bearer-only setup it calls build_anthropic_bedrock_client() for Claude models, which raises RuntimeError: could not resolve credentials from session. Your BedrockConverseAuxiliaryClient + if resolve_aws_auth_env_var() == "AWS_BEARER_TOKEN_BEDROCK" branch is the fix, and resolve_aws_auth_env_var already exists in main's bedrock_adapter, so it rebases cleanly.
  2. _resolve_task_provider_model explicit-provider guard — main still unconditionally returns "custom" when a base_url is present, which routes provider: bedrock + bearer token through the OpenAI /chat/completions wire on a Bedrock host and surfaces as a silent ChatCompletion(id=None, choices=None, ...). Your if cfg_provider and cfg_provider not in {"auto", "custom"} guard (both call sites) is still needed.
  3. _is_unsupported_parameter_error "is deprecated" — main's marker list stops at "unrecognized parameter"; it does not match Bedrock's "temperature is deprecated for this model." phrasing. Without it the reactive-retry branch never fires for Opus 4.7 / Sonnet 4.5 and the ValidationException surfaces as an empty response.

Recommended action

Rebase onto current main, keep commits 58a88f9 + e18476 (aux path + strict base64 validation), drop the parts already on main. Re-run the bedrock test suite (all 284 pass). Then this is a clean, mergeable fix for a real still-live bug class.

When AWS_BEARER_TOKEN_BEDROCK is the auth source, the auxiliary client's
aws_sdk branch routed Claude models to the AnthropicBedrock SDK, which
only supports IAM credentials from the boto3 chain and raises
"RuntimeError: could not resolve credentials from session" on a bearer
token. Route Claude (and everything else) through the existing
BedrockAuxiliaryClient Converse shim in that case — boto3's Converse call
picks up the bearer token natively and supports all Bedrock models. This
mirrors the dual-path routing already present in
hermes_cli.runtime_provider for the main model loop.

Also:
- _is_unsupported_parameter_error now matches Bedrock Converse's
  "`temperature` is deprecated for this model." phrasing so the reactive
  retry fires instead of surfacing an empty all-None ChatCompletion on
  Opus 4.7 / Sonnet 4.5.
- _convert_content_to_converse decodes image data URLs with validate=True
  and skips malformed/empty payloads rather than forwarding the raw base64
  string as bytes (which Bedrock rejects with ValidationException).
@theodore3131
theodore3131 force-pushed the fix/bedrock-aux-vision branch from e184763 to bd906d3 Compare July 31, 2026 04:43
@theodore3131

Copy link
Copy Markdown
Author

Rebased onto current main — now mergeable, scope reduced to the one still-live bug

Force-pushed a clean rebase (bd906d3). The branch was ~4,362 commits behind main; two of the original three fixes had already landed independently, so I dropped them and kept only what's still broken on main:

Dropped (already on main):

  • Main-loop bearer→Converse routing in resolve_runtime_provider — main has the _has_bearer_token guard (this was fix(bedrock): correct api_mode detection for bearer-token auth #26531's commit).
  • Image raw-bytes decode in _convert_content_to_converse — main already does base64.b64decode(data).
  • The _resolve_task_provider_model explicit-provider guard — superseded by main's more general _preserve_provider_with_base_url(), which preserves any catalog provider (incl. bedrock) over the base_url→custom heuristic. Verified get_provider("bedrock") is non-None, so the trap can't fire.

Kept (still broken on main):

  1. Aux-vision bearer-token pathresolve_provider_client's aws_sdk branch still routed Claude to the AnthropicBedrock SDK, which raises RuntimeError: could not resolve credentials from session on a bearer token. Now routes through the existing BedrockAuxiliaryClient Converse shim (reused rather than adding a duplicate BedrockConverseAuxiliaryClient — "extend, don't duplicate").
  2. _is_unsupported_parameter_error now matches Bedrock's "temperature is deprecated for this model." phrasing so the reactive retry fires instead of an empty all-None ChatCompletion.
  3. Base64 image hardeningvalidate=True + skip malformed/empty payloads instead of main's data.encode("utf-8") fallback, which forwards junk bytes that Bedrock rejects with ValidationException.

Tests: added TestBearerTokenAuxRoutesToConverse (aux path) + malformed-image drop tests, and fixed two pre-existing env-dependent tests that assumed no ambient AWS_BEARER_TOKEN_BEDROCK (they fail on clean main too whenever a bearer token is set — now isolated via monkeypatch.delenv). Full bedrock/aux sweep: 567 passed, 3 skipped.

Diff shrank from +937/-9 across 10 files to +130/-11 across 5 files. mergeable is now true.

@theodore3131

Copy link
Copy Markdown
Author

Thanks for the careful review, @teknium1. Both concerns are already addressed on the current HEAD (bd906d33) — they applied to an earlier revision of this PR. Verified against the tip:

1. async bearer-token path_to_async_client now has an explicit Bedrock branch, so it no longer falls through to AsyncOpenAI(base_url=bedrock-runtime...):

agent/auxiliary_client.py:5078
    if isinstance(sync_client, BedrockAuxiliaryClient):
        return AsyncBedrockAuxiliaryClient(sync_client), model

resolve_provider_client('bedrock', ..., async_mode=True) with AWS_BEARER_TOKEN_BEDROCK set now returns the async Converse wrapper, not an OpenAI-wire client.

2. malformed data URL — the decode now uses strict validation and skips the block on failure instead of forwarding junk bytes:

agent/bedrock_adapter.py:592
    try:
        raw_bytes = base64.b64decode(data, validate=True)
    except (binascii.Error, ValueError):
        continue

validate=True rejects out-of-alphabet payloads (e.g. ====!!notvalid), so a malformed image is dropped rather than sent as bytes / blowing up the request.

Both paths have test coverage (async wrapper + malformed-payload skip). @outsourc-e re-ran the touched Bedrock/aux suites — 209 passed. Since the review points are resolved and the branch is mergeable, could this get another look for merge? Happy to rebase if needed.

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/vision Vision analysis and image generation type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants