Skip to content

Scoping a first-class AWS Bedrock provider with native SigV4 auth (follow-up to #50) #659

Description

@cru-Luis-Rodriguez

Following up on #50, where you said:

we don't yet have a dedicated built-in Bedrock preset (with native SigV4 auth, region selection, etc.) — if that's something you'd find valuable and are interested in contributing, we'd welcome a PR adding Bedrock as a first-class provider in internal/llm/providers.go. Happy to help scope it out in an issue if you'd like to discuss the approach first.

Taking you up on the scoping offer. I have this working and tested against a live Bedrock account, and there is one decision I would rather have your view on before opening a PR: the AWS SDK dependency.

The gap the existing paths leave

To be clear about what is already possible, since #50 covered it: the LiteLLM preset and the OpenAI-compatible endpoint both work. The case neither covers is an organisation whose Bedrock access is governed entirely through IAM and SSO.

  • LiteLLM proxy means a process to run in front of every workstation and CI runner. Workable, but it is infrastructure to operate for what is otherwise a self-contained CLI.
  • Custom provider against the OpenAI-compatible endpoint needs a Bedrock API key — a long-lived static credential to generate, distribute and rotate. That is exactly what organisations move to SSO to stop doing. Ours are short-lived credentials from aws sso login, scoped by an IAM permission set; there is no key to put in a config file.

Native SigV4 removes both. ocr then authenticates the same way every other AWS tool on the machine already does, and access is granted or revoked by editing an IAM policy.

Shape of the change

Bedrock serves the same Messages API, so very little is new. bedrock.WithConfig from anthropic-sdk-go handles the signing, the model-into-path rewrite, and anthropic_version injection — and it returns an option.RequestOption, which is exactly the type NewAnthropicClient already assembles a slice of. So AnthropicClient is reused as-is behind a new protocol constant, following the extension contract protocol.go documents for itself.

  • protocol.go — one constant plus its NormalizeProtocol / ValidateProtocol entries
  • providers.go — a bedrock preset, and an AmbientAuth flag marking providers that authenticate from the environment rather than an api_key
  • resolver.go — two checks taught about ambient auth (below)
  • client.goNewAnthropicBedrockClient, essentially the existing constructor minus the auth headers
  • tests, including one asserting api_key is still required for every non-ambient provider

The resolver changes are the part most likely to want your opinion. tryProviderConfig requires a non-empty api_key, and ResolveEndpointWithModelOverride requires both URL and Token before an endpoint counts as complete. Bedrock has none of the three — the region supplies the host, SigV4 supplies the credentials — so a correct config fell through every strategy and surfaced no valid LLM endpoint configured, i.e. the error for having configured nothing at all. I gated both on AmbientAuth rather than special-casing a provider name, but I am not attached to that shape.

Region and profile resolve from the standard AWS chain, with optional aws_region / aws_profile in the provider entry so a run is reproducible without exporting environment variables first.

The dependency question

It adds aws-sdk-go-v2/config as the only direct dependency, and 13 transitive modules with it (credentials, service/sso, service/ssooidc, service/sts, feature/ec2/imds, internal/ini, smithy-go, and a few smaller ones). That is real weight for a provider most users will not enable, and I would understand hesitation.

Options, happy to follow whichever you prefer:

  1. Direct dependency — simplest; one go.mod entry and it works out of the box.
  2. Build tag//go:build bedrock, keeping the SDK out of default builds, at the cost of a second build configuration and a release-artefact decision.
  3. Separate module or plugin — cleanest dependency story, most work; I would want your guidance on placement given extensions/ and plugins/.

I lean toward (1) for usability but will defer to whatever fits your release story. If the answer is (2) or (3), knowing now saves a rewrite.

One thing that may belong in the SDK instead

Worth surfacing because it affects scope. bedrock.WithConfig prefers bearer-token auth whenever cfg.BearerAuthTokenProvider is non-nil, and config.LoadDefaultConfig populates that from the AWS SSO token cache. So any SSO-authenticated caller silently sends its SSO OIDC access token instead of signing, and Bedrock answers:

403 Forbidden {"Message":"Invalid API Key format: Must start with pre-defined prefix"}

Diagnosis is slower than it should be because the SDK reports the pre-middleware URL (/v1/messages) in the error, which makes the path rewriting look broken when it is working correctly.

My patch clears that provider unless AWS_BEARER_TOKEN_BEDROCK was set deliberately. It works, but it is arguably a workaround for an anthropic-sdk-go bug. If you would rather see it fixed there first, that is fair and would shrink the ocr-side patch. I also noticed bedrock/bedrockmantle.go resolves auth by an explicit documented precedence, which reads as the more considered design of the two — possibly relevant if you have a view on which path a built-in preset should take.

Happy to open the PR as soon as you have a preference on the dependency, or to link the branch first if seeing the code would help you decide.

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions