Skip to content

DUX-9411: Add Bedrock bedrock-mantle endpoint support for GPT-5.6 models - #21

Merged
samuelboland merged 7 commits into
mainfrom
DUX-9411-bedrock-mantle-openai-support
Jul 28, 2026
Merged

DUX-9411: Add Bedrock bedrock-mantle endpoint support for GPT-5.6 models#21
samuelboland merged 7 commits into
mainfrom
DUX-9411-bedrock-mantle-openai-support

Conversation

@staging-supernova-dx-appf-io

@staging-supernova-dx-appf-io staging-supernova-dx-appf-io Bot commented Jul 25, 2026

Copy link
Copy Markdown

Summary

Add OpenAI (bedrock-mantle) endpoint support to ruby_llm, enabling access to frontier GPT-5.x models through AWS Bedrock's mantle routing protocol.

Changes

Core Support

  • New MantleResponses protocol (lib/ruby_llm/protocols/mantle_responses.rb): Implements OpenAI Responses API compatibility for Bedrock's bedrock-mantle endpoint, with vendor-prefixed model ID handling and proper SigV4 signing region parameterization
  • Bedrock provider enhancements (lib/ruby_llm/providers/bedrock.rb): Adds mantle protocol registration, mantle endpoint configuration, and routing logic to discriminate between converse/invoke_model and mantle endpoints
  • Auth region parameterization (lib/ruby_llm/providers/bedrock/auth.rb): Fixes signing region to use bedrock_mantle_region when present, enabling cross-region bedrock-mantle requests
  • Connection base_url override (lib/ruby_llm/connection.rb): Adds base_url: parameter to Connection#initialize for protocol-specific endpoint routing

Model Registry

  • GPT-5.6 variants (lib/ruby_llm/models.json): Hand-added entries for GPT-5.6 Sol, Terra, and Luna with Bedrock-specific metadata (272K context, 128K output tokens)
  • Aliases (lib/ruby_llm/aliases.json): Canonical aliases mapping gpt-5.6-{sol,terra,luna} to bedrock vendor-prefixed IDs

Bug Fixes

  • Reasoning model detection (lib/ruby_llm/protocols/responses/chat.rb): Fixed regex to properly match vendor-prefixed reasoning models (e.g., openai.o1, openai.gpt-5) in addition to unprefixed patterns
  • Error parsing robustness (lib/ruby_llm/providers/bedrock.rb): Added TypeError handling for non-JSON error bodies in mantle protocol
  • Parameter validation (lib/ruby_llm/protocols/mantle_responses.rb): Rejects converse-only streaming parameters that bedrock-mantle endpoint doesn't support

Test Coverage

  • Connection base_url override (spec/ruby_llm/connection_base_url_spec.rb): Tests custom base_url routing
  • MantleResponses protocol (spec/ruby_llm/protocols/mantle_responses_spec.rb): Comprehensive coverage of endpoint routing, error handling, and reasoning model support
  • GPT-5.6 models (spec/ruby_llm/models_gpt_5_6_bedrock_spec.rb): Verifies model registry and alias resolution
  • Bedrock integration (spec/ruby_llm/providers/bedrock_spec.rb): Extended with mantle routing and cross-region signing tests
  • Chat protocol (spec/ruby_llm/protocols/responses/chat_spec.rb): Added vendor-prefixed reasoning model detection tests

Related

  • Fixes bedrock_mantle_region configuration to properly parameterize SigV4 signing region
  • Enables GPT-5 frontier model access via bedrock-mantle endpoint
  • Maintains backward compatibility with existing converse/invoke_model protocols

Agent session: https://staging.supernova.dx.appf.io/coders/7f845d85-f6b4-4b5c-a2d1-9525fe86771f

appf-supernova and others added 4 commits July 25, 2026 16:05
…fixed ids

reasoning_model? didn't match the "openai." vendor prefix Bedrock uses
(e.g. openai.gpt-5.6-sol), so encrypted-reasoning replay and the
temperature strip silently didn't kick in for those ids. parse_error
also returned a Hash instead of a message string for OpenAI-shaped
mantle error bodies ({"error": {"message": ...}}).

Co-authored-by: Sam Boland <[email protected]>
Generic plumbing for the upcoming bedrock-mantle protocol: Connection
can now be pointed at a base URL other than the provider's default,
and SigV4 signing can target a service namespace other than "bedrock".
Both default to today's behavior, so every existing caller is
unaffected.

Co-authored-by: Sam Boland <[email protected]>
…e endpoint

openai.gpt-5.x models (the GPT-5.6 Sol/Terra/Luna family) are reachable
only via bedrock-mantle speaking the OpenAI Responses API — no Converse,
no InvokeModel. MantleResponses subclasses Protocols::Responses, binds
to a mantle-specific connection, picks /openai/v1/responses vs
/v1/responses per model, and SigV4-signs against the bedrock-mantle
service namespace instead of bedrock.

Bedrock#protocol_for routes openai.gpt-5* ids to the new protocol ahead
of the existing invoke_model/converse decision — deliberately narrower
than a broad openai. prefix so gpt-oss (bedrock-runtime-native) keeps
routing to Converse. #complete also skips the Converse-specific
normalize_params on the mantle path so caller-supplied
additionalModelRequestFields isn't forwarded to a Responses API call.

Co-authored-by: Sam Boland <[email protected]>
models.dev doesn't have GPT-5.6 yet, and Bedrock.assume_models_exist?
is false, so model resolution would raise without these. Mirrors the
openai.gpt-5.5 entry shape (commit 0ffb655 set this precedent for
claude-opus-5). Kept in its own commit so it's trivially droppable if
these ids land upstream later.

Pricing per million tokens: Sol 5.00/30.00 (cache_read 0.50), Terra
2.50/15.00 (0.25), Luna 1.00/6.00 (0.10) — cache_read at 90% off input
in every tier. reasoning_options includes "max", which gpt-5.5 doesn't
have. knowledge_cutoff is null; no date could be verified.

Co-authored-by: Sam Boland <[email protected]>
@kermitapp

kermitapp Bot commented Jul 25, 2026

Copy link
Copy Markdown

@staging-supernova-dx-appf-io

Copy link
Copy Markdown
Author

🤖 Supernova Code Review — View trace

♻️ Re-run this automation

@staging-supernova-dx-appf-io staging-supernova-dx-appf-io Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Reviewed the bedrock-mantle/GPT-5.6 changes. One actionable bug found: SigV4 signing for mantle requests hardcodes bedrock_region instead of the mantle_region the request host is actually built from, which breaks auth whenever bedrock_mantle_region differs from bedrock_region (see inline comment on auth.rb). Also left a minor duplication nit on the frontier-model regex. Everything else (protocol routing, connection base_url override, error parsing, reasoning-model regex fix, model/alias registry entries) looked correct and is well covered by the added specs.

♻️ Re-run this automation

Comment thread lib/ruby_llm/providers/bedrock/auth.rb Outdated
Comment on lines 14 to 53
@@ -41,15 +41,15 @@ def sign_headers(method, path, body, base_url: api_base)
payload_hash
].join("\n")

credential_scope = "#{date_stamp}/#{bedrock_region}/bedrock/aws4_request"
credential_scope = "#{date_stamp}/#{bedrock_region}/#{service}/aws4_request"
string_to_sign = [
'AWS4-HMAC-SHA256',
amz_date,
credential_scope,
Digest::SHA256.hexdigest(canonical_request)
].join("\n")

signing_key = signing_key(date_stamp, credentials.secret_access_key)
signing_key = signing_key(date_stamp, credentials.secret_access_key, service)
signature = OpenSSL::HMAC.hexdigest('sha256', signing_key, string_to_sign)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

sign_headers (and the signing_key helper it calls) hardcode bedrock_region in the SigV4 credential scope ("#{date_stamp}/#{bedrock_region}/#{service}/aws4_request") and in the region-derived signing key, with no way to pass a different region. But mantle_api_base is built from mantle_region, which can differ from bedrock_region via the supported bedrock_mantle_region config option (see bedrock.rb#mantle_region and the "prefers bedrock_mantle_region over bedrock_region when set" spec).

When an operator sets bedrock_mantle_region to a region different from bedrock_region, MantleResponses#mantle_signature_headers calls sign_headers(..., base_url: @provider.mantle_api_base, service: MANTLE_SIGNING_SERVICE) — the request goes to a host in mantle_region, but the SigV4 signature is computed using bedrock_region. AWS will reject this with a signature/region mismatch, so mantle requests break entirely for any config that actually exercises bedrock_mantle_region. None of the added specs cover a scenario where bedrock_mantle_region != bedrock_region, so this isn't caught.

Fix: thread the signing region through explicitly, e.g. add a region: keyword to sign_headers/signing_key (defaulting to bedrock_region for existing callers) and have mantle_signature_headers pass region: @provider.mantle_region.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If this is still a problem, re-analyze and determine if it needs a fix.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Already fixed — sign_headers/signing_key now take an explicit region: param (defaulting to bedrock_region), and MantleResponses#mantle_signature_headers passes region: @provider.mantle_region, so mantle requests sign correctly even when bedrock_mantle_region differs from bedrock_region. No further action needed here.

# Frontier openai.gpt-5.x models are served at /openai/v1/responses; every other
# mantle model (e.g. openai.gpt-oss-*) is served at /v1/responses. See the
# AWS Bedrock model cards for GPT-5.6.
FRONTIER_GPT5_PATTERN = /\Aopenai\.gpt-5/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

nit: FRONTIER_GPT5_PATTERN here duplicates Bedrock::MANTLE_ONLY_MODEL_PATTERN (same regex, same intent). Since MantleResponses only ever handles models protocol_for already routed via mantle_only_model?, consider referencing Providers::Bedrock::MANTLE_ONLY_MODEL_PATTERN here instead of maintaining two copies that could silently diverge.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good point. Do it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Queued — will dedupe by reusing Bedrock::MANTLE_ONLY_MODEL_PATTERN instead of the separate FRONTIER_GPT5_PATTERN constant.


🔍 View trace

appf-supernova and others added 2 commits July 27, 2026 20:46
…ly works

sign_headers hardcoded bedrock_region in both the credential scope and the
signing key, even though mantle_api_base already derives its host from
mantle_region. Any Sol-tier request (us-east-1/us-east-2) issued from a
us-west-2-configured app would sign against the wrong region and get an
InvalidSignatureException. Add a region: kwarg to sign_headers/signing_key,
default to bedrock_region for full backward compatibility, and thread
provider.mantle_region through MantleResponses. Also make mantle_region
public so the protocol can call it, and extract the duplicated 'bedrock'
default into DEFAULT_SIGNING_SERVICE.

Adds a spec exercising a real signature with mismatched bedrock_region/
bedrock_mantle_region, and a spec asserting the signed X-Amz-Content-Sha256
matches a SHA-256 of the bytes WebMock actually received.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

Co-authored-by: Sam Boland <[email protected]>
…istry provenance/ordering

MANTLE_ONLY_MODEL_PATTERN (Bedrock) and FRONTIER_GPT5_PATTERN (MantleResponses)
answer different questions but coincide today. Comment each pointing at the
other so a future frontier family (e.g. gpt-6-*) doesn't get updated in only
one place, silently 404ing or misrouting. Patterns themselves are unchanged.

Also corrects the hand-added GPT-5.6 registry entries: "source": "models.dev"
was false (models.dev has no gpt-5.6 yet, and that field gates what rake
models preserves on a fetch failure) and "last_updated": "2026-06-01" predated
the entries' own "created_at" of 2026-07-13. Reordered sol/terra/luna to
alphabetical (luna, sol, terra) to match rake models' own sort and avoid a
gratuitous reorder diff whenever it's next run.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

Co-authored-by: Sam Boland <[email protected]>
…y params on mantle

Guard the new error.message dig against a String-valued `error` key (Mantle-style
{"error": "..."} bodies used to hit the body['error'] fallback pre-parse_error-change;
Hash#dig on a String raises). Also make strip_converse_only_params raise a clear
ArgumentError for top_k/additionalModelRequestFields on the mantle path instead of
silently forwarding them to a Responses API that would reject them with an opaque 400.

Add comments cross-referencing the two forward-looking/dead-branch spots flagged in
review: the bare `super` in Bedrock#complete forwards the reassigned `params`, and
MantleResponses#completion_url's non-frontier branch is currently unreachable in
production routing.

Co-authored-by: Sam Boland <[email protected]>
@samuelboland

Copy link
Copy Markdown

Double check that this isn't going to cause issues with exisitng code paths in any way.

@staging-supernova-dx-appf-io

Copy link
Copy Markdown
Author

@samuelboland

Double check that this isn't going to cause issues with exisitng code paths in any way.

Confirmed, checked all call sites on the actual PR branch:

  • sign_headers(method, url, body, base_url:) — the only other call site (line 89, the generic batch-style helper in auth.rb) doesn't pass service:/region:, so it falls through to the new defaults DEFAULT_SIGNING_SERVICE = 'bedrock' and region: bedrock_region — identical to the pre-PR behavior. Existing Converse/InvokeModel/control-plane requests are unaffected.
  • signing_key similarly defaults service/region to the old hardcoded values when called with 2 args, so no other caller needs updating.
  • Only MantleResponses#mantle_signature_headers passes the new service:/region: explicitly, scoping the behavior change to the new mantle path.

So this change is additive via keyword defaults and doesn't alter any existing non-mantle code path.

@samuelboland
samuelboland merged commit 20aeef6 into main Jul 28, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants