🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

feat: add AWS Bedrock mantle endpoint to AI Gateway - #26745

Merged
evgeniy-scherbina merged 17 commits into
mainfrom
yevhenii/add-bedrock-provider
Jul 13, 2026
Merged

feat: add AWS Bedrock mantle endpoint to AI Gateway#26745
evgeniy-scherbina merged 17 commits into
mainfrom
yevhenii/add-bedrock-provider

Conversation

@evgeniy-scherbina

@evgeniy-scherbina evgeniy-scherbina commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Implements https://linear.app/codercom/issue/AIGOV-213/add-bedrock-provider

AWS Bedrock mantle support in AI Gateway

Summary

Add support for the AWS Bedrock mantle endpoint (bedrock-mantle.{region}.api.aws/anthropic/v1/messages) to AI Gateway. Mantle serves Claude through the native Anthropic Messages API. We model it as a protocol field on the existing Bedrock provider settings (invoke-model default, or mantle) rather than as a new provider type, and we treat mantle as a pure passthrough: SigV4-sign and forward, no body translation.

Background

Claude on AWS Bedrock is reachable through two endpoints, each speaking exactly one wire protocol:

  1. InvokeModel (existing): bedrock-runtime.{region}.amazonaws.com. Model id in the URL path, request translated into Bedrock's InvokeModel format, responses returned as a binary AWS eventstream. This is what AI Gateway already supported for Bedrock.
  2. Mantle (this doc): bedrock-mantle.{region}.api.aws/anthropic/v1/messages. Native Anthropic Messages API: model in the body, plain SSE streaming.

Why a protocol field, not a new provider type

The alternative is to model mantle as its own ai_provider_type (bedrock-mantle) alongside bedrock. I chose the protocol field instead for two reasons:

  1. Mantle reads more like a protocol of Bedrock than a separate provider. It is the same AWS account, credentials, region, and IAM, reached over a different wire protocol and host. One Bedrock provider with two protocols (invoke-model default and mantle) models that more organically than two provider types.
  2. It avoids a database migration. The protocol field lives in the settings JSON blob (empty resolves to invoke-model, so existing providers are unaffected), whereas a new type means an enum value and the ALTER TYPE ... ADD VALUE migration that goes with it.

Why passthrough, not translation

The client already emits Bedrock-legal requests in mantle mode:

export CLAUDE_CODE_USE_MANTLE=1
export CLAUDE_CODE_SKIP_MANTLE_AUTH=1
export ANTHROPIC_BEDROCK_MANTLE_BASE_URL=https://<coder>/api/v2/aibridge/<provider-name>

So the gateway just forwards the body and SigV4-signs it (service bedrock-mantle), and skips all the InvokeModel body-translation (model remap, thinking conversion, beta-flag allowlist, field stripping). This keeps the mantle path thin and avoids a second copy of translation logic to maintain.

Consequences

  • Protocol-dependent fields: model / small_fast_model are used by InvokeModel but ignored by mantle (the client sends the model), and base_url is required for mantle but optional for InvokeModel. Validation is protocol-aware.
  • No central model control on mantle: because it is a passthrough, the operator cannot pin the model.
  • region and the base_url host must name the same region (the SigV4 scope must match the endpoint); a mismatch surfaces as Credential should be scoped to a valid region.

Draft UI

image

Follow-up PRs:

@evgeniy-scherbina
evgeniy-scherbina force-pushed the yevhenii/add-bedrock-provider branch 4 times, most recently from 994dd65 to 1bdf7a3 Compare July 9, 2026 19:00
@evgeniy-scherbina
evgeniy-scherbina force-pushed the yevhenii/add-bedrock-provider branch from 5eec1ac to f953c8c Compare July 9, 2026 19:33
@evgeniy-scherbina
evgeniy-scherbina marked this pull request as ready for review July 10, 2026 11:42
@coder-tasks

coder-tasks Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/ai-coder/ai-gateway/providers.md - The Amazon Bedrock section needs to document the new protocol field:
    • The two protocol options: invoke-model (default, legacy behavior) and mantle (passthrough with SigV4 signing)
    • For mantle: both region and base_url are required (unlike invoke-model which accepts either)
    • For mantle: no request/response translation occurs; the gateway only signs and forwards
    • The mantle endpoint format: bedrock-mantle.{region}.api.aws/anthropic/v1/messages
    • The base_url for mantle should be the Messages API prefix without /v1/messages
    • Note that model and small_fast_model are not required for mantle (the client controls the model)

    ℹ️ Docs for this were written and then intentionally removed in bc6ba36 ("chore(aibridge): defer Bedrock mantle UI and docs to follow-up"). The PR description confirms the UI and docs are deferred to a follow-up PR. Tracking this here so the follow-up PR restores the removed content.


Automated review via Coder Agents

@evgeniy-scherbina

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-07-13 21:21 UTC by @evgeniy-scherbina
Spend: $76.57 / $100.00

Review history
  • R1 (2026-07-10): 16 reviewers, 5 Nit, 3 Note, 1 P2, 10 P3, COMMENT. Review
  • R2 (2026-07-13): 13 reviewers, 9 Nit, 3 Note, 1 P2, 11 P3, COMMENT. Review

deep-review v0.9.0 | Round 2 | 5334cc8..7a807a3

Last posted: Round 2, 24 findings (1 P2, 11 P3, 9 Nit, 3 Note), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Author fixed (7419630) base.go:365 Missing nil guard on i.bedrock in withBedrockMantleOptions R1 Netero Yes
CRF-2 P3 Author fixed (6cd6d58, 00e8b76) aiproviders.go:287 Mantle-region and protocol validation missing from UpdateAIProviderRequest.Validate() R1 Netero, Hisoka, Knov, Meruem Yes
CRF-3 Nit Author fixed (proto comment) aibridged.proto:239 Grammar: "fallbacks" should be "falls back" R1 Netero Yes
CRF-4 P2 Author fixed (6cd6d58) aiproviders.go:287 Unknown protocol values pass API validation and silently degrade to unsigned requests R1 Hisoka P2, Mafuuu P2, Knov P2, Meruem P2, Ryosuke P2, Pariston P3, Kurapika P3, Melody P3, Chopper P3 Yes
CRF-5 P3 Author fixed (4db66ee) base_internal_test.go:1132 TestBedrockMantleIsPassthrough claims passthrough but only asserts Model() R1 Bisky Yes
CRF-6 P3 Author fixed (0db20ff) aiproviders_test.go:246 InvokeModelDoesNotRequireRegionField tests nil Bedrock, not InvokeModel protocol R1 Bisky Yes
CRF-7 P3 Author contested; panel closed R2 (7/7 acceptable) base.go:82 appendBedrockPRMUserAgent silently drops attribution when User-Agent is empty R1 Meruem P3, Leorio Note, Ryosuke Note Yes
CRF-8 P3 Author fixed (d941a12) base.go:366 withBedrockMantleOptions returns bare Validate() errors without wrapping R1 Leorio Yes
CRF-9 P3 Author fixed (be4c152) base.go:385 Credential and signing errors in mantle middleware lack diagnostic wrapping R1 Chopper Yes
CRF-10 P3 Author fixed L414; panel closed remainder R2 base.go:72 Five doc comments restate code structure (base.go:72, 151, 359, 370, 414) R1 Gon Yes
CRF-11 P3 Dropped by orchestrator (process feedback, not code) - Feature commit empty body R1 Leorio No
CRF-12 P3 Author contested; panel closed R2 (6/7 benign) base.go:269 Redundant WithBaseURL: generic path sets it then Bedrock paths override R1 Ryosuke Yes
CRF-13 P3 Author fixed (d20f41e) typesGenerated.ts:374 Generated TS comment diverges from Go source (extra "without a schema version bump") R1 Mafu-san Yes
CRF-14 Note Open (silent) base.go:174 Tracing does not distinguish between InvokeModel and mantle protocols R1 Mafuuu Yes
CRF-15 Note Open (silent) base.go:297 Mantle passthrough grants clients model selection authority R1 Kurapika Yes
CRF-16 Note Dropped by orchestrator (structural observation, no action needed) aiproviders_bedrock.go:13 Protocol enum as two separate types with raw cast bridging R1 Meruem No
CRF-17 Note Dropped by orchestrator (low stakes, predicates clearly disjoint) base.go:286 Protocol dispatch uses separate ifs not if/else R1 Hisoka No
CRF-18 Note Open (silent) typesGenerated.ts:378 Mantle providers will appear broken in admin edit form R1 Nami Yes
CRF-19 Nit Open (re-raised R2: PR added 2 more instances) aiproviders_test.go:219 Manual contains check; slices.ContainsFunc available since Go 1.21 R1 Ging-Go Yes
CRF-20 Nit Author fixed (t.Context()) base_internal_test.go:1190 context.Background() where t.Context() available since Go 1.24 R1 Ging-Go Yes
CRF-21 Nit Author fixed (5b0ebfb) base.go:72 BedrockPRMUserAgent doc doesn't expand PRM acronym R1 Leorio Yes
CRF-22 Nit Dropped by orchestrator (process feedback, not code) - ci: make fmt commit uses wrong scope R1 Leorio No
CRF-23 Nit Author fixed (f1bc953) base.go:298 Credential timeout is inline magic number (30s) R1 Gon Yes
CRF-24 Nit Open base_internal_test.go:603 Stale test name TestAugmentRequestForBedrock_AdaptiveThinking after method rename R2 Netero, Mafu-san Yes
CRF-25 P3 Open base.go:301 Protocol dispatch fails open: unknown protocol on non-nil bedrock sends unsigned request (CRF-4 sink not closed) R2 Meruem P3, Kurapika P3, Mafuuu P3 Yes
CRF-26 Nit Open config_test.go:74 Two "mantle valid" test rows exercise same branch; names imply prefix validation that doesn't exist R2 Bisky Yes
CRF-27 Nit Open config.go:78 Validate() says "base url" (InvokeModel) and "base_url" (mantle) for the same field R2 Leorio Yes
CRF-28 Nit Open base.go:77 BedrockPRMUserAgent names a UA suffix not a full UA; invites misuse R2 Gon Yes

Contested and acknowledged

CRF-7 (P3, base.go:82) - appendBedrockPRMUserAgent drops attribution on empty User-Agent

  • Finding: appendBedrockPRMUserAgent only appends the PRM marker when the existing User-Agent is non-empty (ua != ""). If a request has no User-Agent, the marker is silently dropped and AWS partner attribution undercounts Coder traffic.
  • Author defense (R2, PRRC_kwDOGkVX1s7U_ouQ): "I did it to preserve existing behavior." The guard matches the pre-existing InvokeModel path, so this is a deliberate consistency choice.
  • Status: contested. Author engages the mechanism (consistency with existing path) but not the consequence (attribution undercount when UA is empty). Left for the panel.
  • Panel closure (R2, 7/7 acceptable): Chopper traced the UA source: BuildUpstreamHeaders rebuilds the outbound header set from the client's headers, so the UA is client-supplied, not the SDK default. The undercount is bounded to requests with no client UA; Claude Code (the target mantle client) always sets one, confirmed by the integration test asserting the marker is present. Consistent with the pre-existing InvokeModel guard, so no new exposure. The one-line unconditional-set fix remains available if attribution completeness is later required; that is a human decision, not blocking this PR.

CRF-12 (P3, base.go:269) - Redundant WithBaseURL

  • Finding: WithBaseURL(i.cfg.BaseURL) is set unconditionally, then overridden by both Bedrock paths. Correctness holds only because bedrockConfig copies the base URL; if they diverge, behavior depends on option append order.
  • Author defense (R2, PRRC_kwDOGkVX1s7VAzvr): "This approach already existed, I think it's out of scope of this PR to change." The pattern predates this PR.
  • Status: contested. Left for the panel.
  • Panel closure (R2, 6/7 benign): Chopper, Hisoka, Mafuuu, Pariston, and Ryosuke traced production: bedrockConfig(spec.BaseURL, ...) copies the provider base URL into AWSBedrock.BaseURL, and i.cfg.BaseURL is the same spec.BaseURL, so the two WithBaseURL values are always equal and last-write-wins has no observable effect. Meruem re-raised at P3 citing genuine divergence, but that divergence exists only in the integration-test harness (which sets the two URLs differently to prove the mantle path owns its endpoint), not in production. Latent fragility acknowledged: if a future change derives the two base URLs differently, correctness would depend on append order. Structural suggestion (set the base URL once in the protocol-specific builder) noted for the author; not gating.

CRF-10 (P3, base.go:72) - Doc comments restate code structure (partial)

  • Finding: Five doc comments restate what code/identifiers already show (L72, 151, 359, 370, 414).
  • Author defense (R2, PRRC_kwDOGkVX1s7VAwgV): Fixed L414 (future tense to present). "Rest seems reasonable to me."
  • Status: L414 addressed; remaining four contested. Completeness is the panel's call.
  • Panel closure (R2): Gon (original author of the finding), Leorio, Mafu-san, Mafuuu, and Pariston agree the four remaining comments carry invariant/why/contract content (the Model() remap rationale, the predicate docs, the const docs), not pure restatement. Comment-density is an editorial call the author is entitled to make. Closed.

Round log

Round 1

Panel. 1 P2, 9 P3, 2 Note, 4 Nit new. 4 dropped. Reviewed against f84801e..989e298.

Round 2

Churn guard: PROCEED. 13 addressed, 2 contested (CRF-7, CRF-12), CRF-10 partial, 4 silent (CRF-14, CRF-15, CRF-18 notes; CRF-19 nit). New head 7a807a3 (rebased base 5334cc8). Panel re-review pending.

Round 2 panel

13 reviewers. All 13 R1 P-level fixes verified genuine. Contested findings closed: CRF-7 (7/7 acceptable, bounded attribution undercount, Claude Code always sets UA), CRF-12 (6/7 benign, both base URLs derive from spec.BaseURL; Meruem's re-raise rested on a test-artifact divergence disproved by production tracing), CRF-10 remainder (kept comments carry semantics). New: CRF-25 (P3, fail-open dispatch, convergent Meruem/Kurapika/Mafuuu), CRF-24/26/27/28 (Nit). Ryosuke's base_url-on-update reopen of CRF-2 was investigated and REJECTED: base_url is unconditionally required on create (aiproviders.go:256) and cannot be cleared on update, so no provider can carry an empty base_url; Chopper, Hisoka, Pariston independently confirmed. Reviewed against 5334cc8..7a807a3.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot 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.

The protocol discriminator is cleanly layered: codersdk types flow through proto to config.BedrockProtocol to the dispatch predicates, with the empty value resolving to InvokeModel at every hop. The mantle signing middleware correctly appends last so it signs after all header mutations, and the passthrough semantics are enforced structurally since the mantle path never calls augmentRequestForBedrockInvokeModel. Test density is healthy at 57.7%, and the integration test proves the real SigV4 signing with the correct service name.

Severity count: 1 P2, 9 P3, 2 Note, 4 Nit.

The P2 is convergent across 9 reviewers: unknown protocol values pass API validation and silently produce unsigned requests at runtime. The config.AWSBedrock.Validate() default case exists but is structurally unreachable for this failure mode. A protocol enum check at the API boundary would close it.

Process observations: the feature commit (f953c8c) has no body, which makes git log and git blame less useful for a 504-line change that introduces a new wire protocol. The follow-up commit (989e298) uses ci: make fmt but ci: denotes pipeline changes; style: or chore: fits better. Neither is blocking.

"If someone accidentally added augmentation to the mantle path, Model() would still return the client's model, and this test would still pass. The fields in the payload look like they were placed there to prove passthrough, then nobody wrote the assertions." (Bisky)


aibridge/intercept/messages/base.go:174

Note [CRF-14] Tracing records IsBedrock as a boolean (i.bedrock != nil), which is true for both InvokeModel and mantle. When debugging production issues, operators cannot tell from traces which wire protocol a request used. These are meaningfully different code paths (translate vs passthrough). A tracing.BedrockProtocol string attribute would make the distinction observable.

(Mafuuu)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread codersdk/aiproviders.go Outdated
})
}
// The Mantle protocol signs requests with SigV4, which requires a region.
if req.Settings.Bedrock.ResolvedProtocol() == AIProviderBedrockProtocolMantle && req.Settings.Bedrock.Region == "" {

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.

P2 [CRF-4] Unknown protocol values pass API validation and silently degrade to unsigned requests at runtime.

Validate() checks that mantle requires a region but never rejects an unknown Protocol string. An operator who sets protocol: "mnatle" (typo) passes validation. At runtime, isBedrockInvokeModel() matches only "" and "invoke-model", isBedrockMantle() matches only "mantle". The unknown value matches neither, so neither signing branch in newMessagesService fires. The request proceeds to the upstream unsigned.

config.AWSBedrock.Validate() has a default reject clause, but it lives inside withBedrockInvokeModelOptions and withBedrockMantleOptions, which are never entered because the dispatch predicates already excluded the unknown value. The validation is unreachable for this failure mode.

"An operator who sets protocol: "tyop" passes validation. The value is stored. At request time, the request falls through to the generic Anthropic path with no SigV4 signing. AWS returns 403, and the operator gets a confusing error with no mention of the invalid protocol." (Pariston P3)

The same gap applies to UpdateAIProviderRequest.Validate() (line 335), which does not inspect the protocol field at all.

Fix in both Create and Update validators:

switch req.Settings.Bedrock.Protocol {
case "", AIProviderBedrockProtocolInvokeModel, AIProviderBedrockProtocolMantle:
default:
    validations = append(validations, ValidationError{
        Field:  "settings.protocol",
        Detail: fmt.Sprintf("unsupported bedrock protocol %q", req.Settings.Bedrock.Protocol),
    })
}

(Hisoka P2, Mafuuu P2, Knov P2, Meruem P2, Ryosuke P2, Pariston P3, Kurapika P3, Melody P3, Chopper P3)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: 6cd6d58

Comment thread codersdk/aiproviders.go Outdated
})
}
// The Mantle protocol signs requests with SigV4, which requires a region.
if req.Settings.Bedrock.ResolvedProtocol() == AIProviderBedrockProtocolMantle && req.Settings.Bedrock.Region == "" {

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.

P3 [CRF-2] UpdateAIProviderRequest.Validate() (line 335) does not check the protocol field at all. An update that sets protocol=mantle without providing a region, or sets an unknown protocol value, passes validation. The same protocol-enum check from CRF-4 and the mantle-region check from line 287 should be replicated in the Update validator.

(Netero, Hisoka, Knov, Meruem)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed:

// request (service "bedrock-mantle") and forwards it; the response is plain
// SSE.
func (i *interceptionBase) withBedrockMantleOptions(ctx context.Context) ([]option.RequestOption, error) {
cfg := i.bedrock.Cfg

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.

P3 [CRF-1] Missing nil guard on i.bedrock. The sibling function withBedrockInvokeModelOptions (line 323) guards with if i.bedrock == nil { return nil, xerrors.New("nil bedrock runtime") }. This function dereferences i.bedrock.Cfg directly. The caller guards via isBedrockMantle() which requires i.bedrock != nil, so this won't panic in production. The risk is a nil-dereference panic if called directly without the caller guard.

(Netero)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: 7419630


i := &interceptionBase{
reqPayload: mustMessagesPayload(t,
`{"model":"anthropic.claude-opus-4-8","max_tokens":10000,"thinking":{"type":"adaptive"},"metadata":{"user_id":"u123"},"context_management":{"type":"auto"}}`),

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.

P3 [CRF-5] Test claims passthrough but only asserts Model().

The payload includes thinking, metadata, and context_management fields that augmentRequestForBedrockInvokeModel would strip or rewrite for InvokeModel. The point of mantle is that these fields survive untouched, but no assertion verifies it. If someone accidentally added augmentation to the mantle path, Model() would still return the client's model (it reads from reqPayload for mantle, from config for InvokeModel), and this test would still pass.

Add an assertion that i.reqPayload is unchanged after the mantle code path runs, or remove the fields that aren't being tested.

(Bisky)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: 4db66ee


create.Settings.Bedrock.Region = "us-east-1"
require.False(t, hasFieldError(create.Validate(), "settings.region"))
})

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.

P3 [CRF-6] This test is titled "InvokeModelDoesNotRequireRegionField" but Settings: codersdk.AIProviderSettings{} leaves Bedrock nil, so Validate() never enters the if req.Settings.Bedrock != nil block. The protocol check is never reached.

Set Bedrock: &codersdk.AIProviderBedrockSettings{} (zero value resolves to InvokeModel via ResolvedProtocol()) so the test enters the protocol-gated branch and proves the region check is scoped to mantle.

(Bisky)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: 0db20ff

if v.Field == field {
return true
}
}

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.

Nit [CRF-19] hasFieldError is a manual loop that slices.ContainsFunc (available since Go 1.21) replaces. The project already uses it in tests.

hasFieldError := func(vs []codersdk.ValidationError, field string) bool {
    return slices.ContainsFunc(vs, func(v codersdk.ValidationError) bool {
        return v.Field == field
    })
}

(Ging-Go)

🤖

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.

Re-raising [CRF-19]: this round added two more hand-rolled []ValidationError loops (hasProtocolError and another hasFieldError) rather than reducing the pattern. slices.ContainsFunc (Go 1.21, go.mod is 1.26) collapses each to one line. Still a nit, not a bug; flagging only because the sibling count grew.

🤖

Creds: credentials.NewStaticCredentialsProvider("test-key", "test-secret", ""),
},
}
opts, err := base.withBedrockMantleOptions(context.Background())

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.

Nit [CRF-20] context.Background() used where t.Context() is available since Go 1.24. The same PR's integration test already uses t.Context() for the mantle path.

(Ging-Go)

🤖

Comment thread coderd/aibridged/proto/aibridged.proto Outdated
string role_arn = 6;
string external_id = 7;
// protocol selects the Bedrock wire protocol ("invoke-model" or "mantle").
// Empty fallbacks to invoke-model.

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.

Nit [CRF-3] Grammar: "Empty fallbacks to invoke-model" should be "Empty falls back to invoke-model." ("fallbacks" is a noun; the verb form is "falls back.")

(Netero)

🤖

Comment thread aibridge/intercept/messages/base.go Outdated
}

if i.isBedrockMantle() {
ctx, cancel := context.WithTimeout(ctx, time.Second*30)

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.

Nit [CRF-23] time.Second*30 is a bare literal in both the mantle block (here) and the InvokeModel block (line 287). A named constant like bedrockCredentialTimeout would make the intent clear and reduce the duplication.

(Gon)

🤖

if ua := req.Header.Get("User-Agent"); ua != "" {
req.Header.Set("User-Agent", ua+" "+BedrockPRMUserAgent)
}
}

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.

Nit [CRF-21] BedrockPRMUserAgent doc comment doesn't expand PRM. PRM is the ISV Partner Registration Marker (or Partner Registration Manager, depending on the AWS doc vintage). The value itself is a percent-encoded APN token, adding another unexplained acronym. A one-line expansion in the doc comment saves someone a search:

// BedrockPRMUserAgent is Coder's AWS Partner Registration Marker (PRM),
// appended to Bedrock User-Agent headers for AWS ISV attribution.

(Leorio)

🤖

@linear-code

linear-code Bot commented Jul 13, 2026

Copy link
Copy Markdown

AIGOV-213

var body []byte
if req.Body != nil {
var err error
body, err = io.ReadAll(req.Body)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we ever set any upper limit on request body before this? If not, is that something to consider?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines +404 to +407
hash := sha256.Sum256(body)
if err := signer.SignHTTP(req.Context(), creds, req, hex.EncodeToString(hash[:]), bedrockMantleSigningService, cfg.Region, time.Now()); err != nil {
return nil, err
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what cases would callers be passing a nil request body here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The body shouldn't be nil here; this is just a defensive check to avoid a panic in io.ReadAll function.

Comment thread coderd/aibridged/proto/aibridged.proto
Comment thread codersdk/aiproviders.go Outdated
})
}
// The Mantle protocol signs requests with SigV4, which requires a region.
if req.Settings.Bedrock.ResolvedProtocol() == AIProviderBedrockProtocolMantle && req.Settings.Bedrock.Region == "" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking: do we need any validation around Settings.Bedrock.Region, or is just ensuring non-empty sufficient? My gut says it is.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think ensuring it's non-empty is sufficient. That's what we've done throughout the codebase.

@evgeniy-scherbina

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot 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.

Round 2. The author addressed all 13 P-level findings from round 1 with dedicated, root-cause fixes, and the panel verified each against the code rather than taking the fix commits on faith. Highlights: the unknown-protocol P2 (CRF-4) is now rejected at both the API layer (validateAIProviderBedrockProtocol, Create and Update) and the config layer; the passthrough test (CRF-5) now asserts the request body is byte-identical after newMessagesService, proving mantle skips augmentation rather than just dispatching differently; and the mantle signing errors (CRF-8, CRF-9) now carry bedrock mantle config: / mantle SigV4: context. Test density rose to 60.6%.

Severity count (new this round): 1 P3, 5 Nit. No P0-P1.

The one new substantive finding is convergent across three reviewers: the CRF-4 fix closed the API entry point but not the runtime dispatch sink. newMessagesService uses two independent if guards with no else/default, so a Bedrock provider whose protocol is neither known value matches neither branch and the request is sent unsigned. This is defense-in-depth today (API validation blocks unknown values at write time), but version skew (a newer coderd stores a future protocol, then the aibridged binary is rolled back) or a non-API config source reaches the sink and fails open. See CRF-25.

Contested findings, panel disposition: CRF-7 (empty-User-Agent drops the PRM marker) is accepted; the undercount is bounded to requests with no client UA, and Claude Code always sets one. CRF-12 (redundant WithBaseURL) is benign; both base URLs derive from the same spec.BaseURL in production, so Meruem's divergence concern is a test-harness artifact, not a production reality. CRF-10's remaining doc comments are an editorial call the author is entitled to make. All three closed.

One investigation worth recording: Ryosuke raised that switching a base-URL-less provider to mantle via PATCH would persist a broken config, since the Update validator checks region but not base_url. Chopper, Hisoka, and Pariston independently checked and rejected it: base_url is unconditionally required on create and cannot be cleared on update, so no provider can carry an empty base_url. Not a live gap.

Still-open notes from round 1, all non-gating and worth a follow-up decision, not a change here: CRF-14 (tracing does not distinguish the two Bedrock protocols), CRF-15 (mantle passthrough shifts model-selection authority to the client), CRF-18 (the admin edit form breaks for mantle providers until follow-up #27156).

Process: two commits are titled CR's fixes, dropping the type(scope): message convention the other fix commits on this branch follow (flagged by Leorio and Mafu-san). Harmless if squash-merged, but the branch history reads better with real subjects.

"I came to fight this and it fought back. Every thread I pulled crossed the diff and held." (Hisoka)


aibridge/intercept/messages/base_internal_test.go:603

Nit [CRF-24] TestAugmentRequestForBedrock_AdaptiveThinking kept its old name after the method it exercises was renamed to augmentRequestForBedrockInvokeModel (and the call site inside the test was updated). After this PR the augmentation is InvokeModel-specific, so the generic name no longer names the path it covers; a reader grepping for tests of augmentRequestForBedrockInvokeModel will not find this by name. Rename to TestAugmentRequestForBedrockInvokeModel_AdaptiveThinking.

(Netero, Mafu-san)

🤖

🤖 This review was automatically generated with Coder Agents.

i.augmentRequestForBedrockInvokeModel()
}

if i.isBedrockMantle() {

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.

P3 [CRF-25] Protocol dispatch fails open: an unknown Protocol on a non-nil bedrock runtime matches neither branch, so newMessagesService builds a client with no AWS signing and forwards the request unsigned.

The CRF-4 fix (marked Author fixed) closed the codersdk API entry point via validateAIProviderBedrockProtocol. The runtime sink is still open. isBedrockInvokeModel() matches ""/"invoke-model", isBedrockMantle() matches "mantle"; the two if blocks here have no else/default. For any other value, neither withBedrockInvokeModelOptions nor withBedrockMantleOptions runs, so AWSBedrock.Validate() (whose default case rejects unknown protocols) is never reached, and the request goes out with only the generic WithBaseURL from line 269, unsigned.

"a newer coderd validates and stores a future protocol value (e.g. \"mantle-v2\"); the binary is rolled back to this version. ResolvedProtocol() passes the value through verbatim, bedrockConfig casts it into the runtime config unchanged, and every request for that provider is sent unsigned. The operator sees opaque upstream 403s, not a config error." (Mafuuu P3)

Rated P3 because API validation blocks the value at write time today; this is the defense-in-depth gap that makes drift, version skew, or a direct settings write fail open silently instead of loud. Fix: make the dispatch exhaustive, a switch on the protocol guarded by i.bedrock != nil with a default that returns an error, or call cfg.Validate() once before the branches so an unrecognized protocol fails closed at signing.

(Meruem P3, Kurapika P3, Mafuuu P3)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed here: #27234

//
// It is appended to Bedrock User-Agent headers so AWS can recognize the
// traffic as Coder-associated Bedrock usage.
const BedrockPRMUserAgent = "sdk-ua-app-id/APN_1.1%2Fpc_cdfmjwn8i6u8l9fwz8h82e4w3%24"

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.

Nit [CRF-28] BedrockPRMUserAgent names a User-Agent suffix, not a full User-Agent. The value is appended (ua+" "+BedrockPRMUserAgent), and the only assembler (appendBedrockPRMUserAgent) is unexported, so someone reaching for the exported const could reasonably write req.Header.Set("User-Agent", BedrockPRMUserAgent) and ship a malformed UA. Rename to BedrockPRMUserAgentSuffix (or ...Marker) so the name states what it is; the doc comment already calls it an appended marker.

(Gon)

🤖

Comment thread aibridge/config/config.go
switch c.Protocol {
case "", BedrockProtocolInvokeModel:
if c.Region == "" && c.BaseURL == "" {
return xerrors.New("region or base url required")

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.

Nit [CRF-27] The same Validate() function names one field two ways: the InvokeModel branch returns "region or base url required" (space, prose) and the new mantle branch returns "base_url required" (underscore, field form). Only base_url matches what the operator types in settings. The base url string is pre-existing, but the mantle branch is new and sits a few lines away, so aligning them is this PR's to make. Suggest "region or base_url required" so both point at the field the operator edits.

(Leorio)

🤖

},
},
{
name: "mantle valid proxy api prefix",

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.

Nit [CRF-26] The two "mantle valid" rows (official api prefix at L67, proxy api prefix at L74) both land in case BedrockProtocolMantle, which only checks Region != "" and BaseURL != "". The BaseURL value never changes the branch, so the second row adds no coverage, and the official/proxy names imply the validator distinguishes an official bedrock-mantle.*.api.aws prefix from an arbitrary proxy URL. It does not. A reader who later adds prefix validation will believe it is already tested. Collapse to one row, or rename both to describe what is actually asserted (any non-empty base URL passes).

(Bisky)

🤖

@evgeniy-scherbina
evgeniy-scherbina merged commit 63ec93a into main Jul 13, 2026
28 of 29 checks passed
@evgeniy-scherbina
evgeniy-scherbina deleted the yevhenii/add-bedrock-provider branch July 13, 2026 23:44
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants