Skip to content

Commit 4ee27d9

Browse files
st-grclaude
andauthored
feat(sandbox,providers): add aws-bedrock as a recognized inference provider (#1704)
* feat(sandbox): allow AWS Bedrock InvokeModel paths through the L7 router Adds two patterns to `default_patterns()` so the supervisor's L7 inference router recognizes the Bedrock InvokeModel URL shape and forwards matched requests to the registered upstream: - `POST /model/{modelId}/invoke` → aws_bedrock_invoke - `POST /model/{modelId}/invoke-with-response-stream` → aws_bedrock_invoke_stream The `{modelId}` segment is wildcarded by extending `detect_inference_pattern` to handle one middle `/*/` segment in addition to the existing trailing `/*`. The wildcard is constrained to a single non-empty path segment to avoid path-traversal liabilities — `/model//invoke` and `/model/a/b/invoke` both no-match. Without this, sandboxes running Claude Code in its native Bedrock mode (`CLAUDE_CODE_USE_BEDROCK=1`, `ANTHROPIC_BEDROCK_BASE_URL`, AWS-style auth) hit the supervisor with `403 connection not allowed by policy` because their URL doesn't match `/v1/*` shapes. The fix unblocks operators wanting to register direct AWS Bedrock, an in-cluster Bedrock-compatible bridge, or a Bedrock-emulating LiteLLM as `--type aws-bedrock` providers. Tests cover: positive matches for invoke + invoke-with-response-stream, query-string handling, GET rejection, empty-segment rejection, multi-segment rejection, and unknown-action rejection. Companion changes (provider discovery spec + YAML profile) follow in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * feat(providers): add aws-bedrock provider profile + discovery spec Adds `aws-bedrock` to the built-in provider catalog so operators can run `openshell provider create --type aws-bedrock --credential ...` and have the gateway treat it as a first-class inference provider alongside `anthropic`, `openai`, etc. - `providers/aws-bedrock.yaml`: YAML profile declaring four credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_REGION). Default endpoint is `bedrock-runtime.us-east-1.amazonaws.com:443`; operators in other regions or running against a Bedrock-compatible proxy override via the operator-supplied `BEDROCK_BASE_URL` config-key (mirrors `ANTHROPIC_BASE_URL` for the `anthropic` provider). - `crates/openshell-providers/src/providers/aws_bedrock.rs`: the `ProviderDiscoverySpec` so `openshell provider create --auto-providers` picks up AWS_* env vars from local credentials. - `crates/openshell-providers/src/providers/mod.rs`: register the module. - `crates/openshell-providers/src/lib.rs`: register the SPEC in the default registry alongside the other providers. - `crates/openshell-providers/src/profiles.rs`: include the new YAML in `BUILT_IN_PROFILE_YAMLS`. What this PR explicitly does NOT add (intentionally separated for review-size reasons; will follow up): - A SigV4 signer in `openshell-router`. The current change simply declares the protocol; a follow-up PR adds outbound SigV4 signing using the `aws-sigv4` crate and a new `auth_style: sigv4` validator branch in profiles.rs. Operators who don't need SigV4 (e.g. an in-cluster bridge that ignores it and authenticates separately to the upstream) can use this PR today. - Body translation between Bedrock InvokeModel shape and other inference shapes. The router treats Bedrock requests as opaque pass-through; if the operator's upstream is real AWS Bedrock it speaks Bedrock natively, if it's a translating bridge the bridge does any conversion server-side. - `BEDROCK_BASE_URL` placeholder substitution in the YAML loader. Today the YAML's `host` is a literal default; operators override with the config-key the same way `ANTHROPIC_BASE_URL` works. Tested: `cargo test -p openshell-providers` (35 tests green) and `cargo test -p openshell-sandbox --lib l7::inference` (40 tests green including the seven new aws_bedrock cases from the previous commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * revert(providers): drop legacy aws-bedrock SPEC, rely on v2 YAML profile Addresses johntmyers's review on #1704: net-new providers should land via the v2 YAML profile only and should NOT require changes to the legacy `ProviderDiscoverySpec` registry. - Delete `crates/openshell-providers/src/providers/aws_bedrock.rs` (the legacy SPEC + `test_discovers_env_credential!` invocation). - Drop `pub mod aws_bedrock;` from `crates/openshell-providers/src/providers/mod.rs`. - Drop `registry.register(providers::aws_bedrock::SPEC)` from `crates/openshell-providers/src/lib.rs`. Kept: - `providers/aws-bedrock.yaml` and the `include_str!` in `BUILT_IN_PROFILE_YAMLS` (`profiles.rs`) — the v2 path. `discover_from_profile()` (`crates/openshell-providers/src/discovery.rs`) picks up AWS_* env vars via `discovery.credentials` in the YAML. - L7 router patterns in `crates/openshell-sandbox/src/l7/inference.rs` — orthogonal to the provider registry. The discovery test in the deleted file goes with it; v2 doesn't have an established per-provider env-var-pickup unit test pattern, and other YAML-only registrations (none today, but this is the new direction) won't carry one either. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * fix(providers): include aws_session_token in discovery + update profile assertion Two fixes from johntmyers's gator-agent re-check on #1704: 1. `providers/aws-bedrock.yaml`: add `aws_session_token` to `discovery.credentials`. The credential is declared in the profile but was missing from the discovery scan list, so Providers v2 `--from-existing` would silently drop temporary AWS credentials (STS / IRSA scenarios). 2. `crates/openshell-server/src/grpc/provider.rs`: update the static `list_provider_profiles_returns_built_in_profile_categories` assertion to include `aws-bedrock` at alphabetical position 0. Adding `providers/aws-bedrock.yaml` to BUILT_IN_PROFILE_YAMLS made the prior `["claude-code", "github", "nvidia"]` expectation stale. Remaining blockers from the same review (deferred to follow-up commits): `inference::profile_for` registration for aws-bedrock, user-facing provider + inference-routing docs, and an `upsert_cluster_inference_route` integration test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * feat(inference): register aws-bedrock profile (bridge-fronted) + docs Addresses johntmyers's blocking review feedback on PR #1704: "aws-bedrock still is not wired into the managed inference.local route registry. profile_for only registers openai, anthropic, and nvidia, so inference set --provider <aws-bedrock-provider> will reject this provider before the new sandbox L7 patterns can be used." Approach: register aws-bedrock as a *bridge-fronted* upstream — the router does not inject any auth header on outbound requests; the configured BEDROCK_BASE_URL is expected to point at a translating bridge / Bedrock-compatible proxy that handles auth in its own pod. This is the shape the L7 patterns commit (8b30211) and the YAML profile (6b51e1a) were designed for. SigV4 signing for direct AWS Bedrock is a separate follow-up; see PR thread. Changes: - core::inference::AuthHeader: add `None` variant for upstreams that authenticate themselves. - core::inference: add AWS_BEDROCK_PROFILE static + register in profile_for. Default base URL is bedrock-runtime.us-east-1, override via BEDROCK_BASE_URL config-key (mirrors ANTHROPIC_BASE_URL pattern). Empty credential_key_names + auth: None means no router-side credential lookup at route time. - router::backend: handle AuthHeader::None as a no-op (skip auth injection). - server::inference::resolve_provider_route: gate find_provider_api_key on auth != None. aws-bedrock providers with empty credentials now resolve cleanly. Updated the unsupported-type error message to include aws-bedrock in the supported list. - server::inference tests: add positive upsert_cluster_route_succeeds_for_aws_bedrock_without_api_key test covering the new code path end-to-end (provider with empty creds + BEDROCK_BASE_URL config → upsert succeeds → resolved route has empty api_key + provider_type aws-bedrock + bridge URL). - core::inference tests: profile_for_known_types covers aws-bedrock, case-insensitive lookup, plus three new aws-bedrock-specific tests (auth: None, no credential keys, bedrock-specific protocols). - docs/sandboxes/inference-routing.mdx: header forwarding row mentions aws-bedrock has no passthrough headers; new tabs in Supported API Patterns (InvokeModel + InvokeModelWithResponseStream) and Create a Provider (with the bridge-fronted shape note + SigV4 deferral). - docs/sandboxes/manage-providers.mdx: new row in Supported Provider Types table; new row in Supported Inference Providers table. Verification (in dev container): - cargo check -p openshell-core -p openshell-router -p openshell-server: clean - cargo test -p openshell-core --lib inference: 14/14 pass (incl. 3 new) - cargo test -p openshell-server --lib inference::tests::upsert: 6/6 pass (incl. new aws-bedrock test) - cargo fmt --check: clean - cargo clippy --all-targets -D warnings: clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * fix(aws-bedrock): bridge-only YAML, doc actual cmd shape, neg test Addresses four findings from gator-agent's #1704 re-check on 4ab587f: - **Item 5** (YAML collects unused AWS creds): mark all four AWS credentials `required: false` and clear `discovery.credentials`. Bridge-fronted routing intentionally does not consume AWS credentials, so `--from-existing` no longer scans for them. The credentials remain in the schema (not deleted) so the SigV4 follow-up can flip them back without a schema migration. Added a multi-line description that names the bridge-fronted shape and the SigV4 deferral so readers don't have to cross-reference the PR thread. - **Item 3** (docs show command that the CLI rejects): rewrite the Create-a-Provider example for AWS Bedrock to use the actual required shape — placeholder `--credential AWS_ACCESS_KEY_ID= unused-bridge-fronted-shape` plus the `--config BEDROCK_BASE_URL`. The placeholder satisfies the gRPC handler's `provider.credentials.is_empty()` rejection without expanding server-side validation; the router ignores it on the outbound path because `auth: AuthHeader::None` skips header injection. Operators see a clearly-labeled placeholder in `provider get` output. - **Item 1** (validator probe): document `--no-verify` as required for `openshell inference set --provider <aws-bedrock>` since the default validation probe doesn't recognize the `aws_bedrock_invoke` / `aws_bedrock_invoke_stream` protocols. Doc now shows the full `provider create` + `inference set --no-verify` flow with rationale for both decisions inline. - **Item 6** (docs polish): `inference-routing.mdx` summary row now lists AWS Bedrock alongside NVIDIA, Anthropic, Vertex AI, and OpenAI-compatible providers, with the bridge-fronted caveat inline. Test additions in `crates/openshell-server/src/inference.rs`: - Renamed the existing aws-bedrock test from `..._without_api_key` to `..._with_bridge_url` and updated it to use a placeholder credential (mirroring the doc-recommended pattern operators will copy-paste). The `auth: None` path still produces an empty `api_key` on the resolved route — the test now documents that the credential is *stored* but not *used*. - Added `upsert_cluster_route_rejects_aws_bedrock_without_bedrock_base_url`: the negative half of johntmyers' "successfully used by upsert_cluster_inference_route or intentionally rejected with a clear documented error" ask. With `default_base_url: ""` and no `BEDROCK_BASE_URL` config, route resolution returns `InvalidArgument` naming the missing base_url rather than silently forwarding prompts to AWS Bedrock with no usable auth. Verification (in dev container): - cargo test -p openshell-core --lib inference: 18/18 (incl. 3 new) - cargo test -p openshell-server --lib inference::tests::upsert: 8/8 (incl. 2 new aws-bedrock cases — positive + negative) - cargo fmt --check: clean - cargo clippy --all-targets -D warnings: clean Item 2 (router-side enforcement of operator-configured Bedrock model path, replacing the current verbatim path forwarding + body-only model rewrite) is the remaining blocker and is genuinely separable — it touches the L7 router with streaming-aware test coverage. Deferring to its own commit so the security-critical change gets the review attention it deserves. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * fix(router): enforce operator-configured Bedrock model in request path Closes the security-blocking item from gator-agent's #1704 re-check on 4ab587f: "Bedrock carries the model id in /model/{modelId}/invoke, but the router currently forwards the caller's original path and only rewrites JSON body model. That lets sandbox code choose a different upstream model than the operator-configured route model, and may also mutate native Bedrock request bodies incorrectly." Two changes in `prepare_backend_request`: 1. **Path rewrite for Bedrock routes.** Before computing the upstream URL, parse the inbound path's `/model/<id>/invoke[-with-response-stream]` shape and substitute the operator-configured `route.model` for the caller-supplied model segment. Sandbox code that hardcodes a different model still works (we don't reject on mismatch), but the operator's configured model is what reaches the upstream / bridge. If the inbound path is somehow not a recognized Bedrock shape on a Bedrock route (the L7 pattern detector upstream of the router should never produce this combination), reject with RouterError::Internal naming the offending path rather than forwarding verbatim. 2. **Skip body-model injection for Bedrock routes.** The existing body rewriter unconditionally inserts `route.model` into the JSON body for non-Vertex routes. AWS Bedrock InvokeModel encodes the model in the URL path; the body is the raw provider-specific payload (Anthropic Messages for Claude, Mistral payload for Mistral, etc.) and must not be mutated. The branch ordering is now: needs_vertex_anthropic_version → strip body model + inject anthropic_version; route_is_bedrock → leave body alone; else → inject route.model (existing default). New helpers, all in `crates/openshell-router/src/backend.rs`: - `route_is_bedrock(route)` — true when route.protocols contains aws_bedrock_invoke or aws_bedrock_invoke_stream. - `parse_bedrock_invocation_path(path)` — returns Some((model_id, "/invoke" | "/invoke-with-response-stream")) for paths matching the recognized Bedrock shapes. Strips query strings. Rejects empty model ids and multi-segment ids (defense-in-depth matching the L7 pattern detector's existing guards). - `rewrite_bedrock_path(route, path)` — returns the path with the caller's model segment replaced by route.model. Test coverage in the same file (9 new tests): - parse_bedrock_invocation_path: positive cases for both invoke variants, query-string stripping; negative cases for empty model id, multi-segment id, unknown action, wrong prefix, missing slash. - route_is_bedrock: matches both protocol variants singly and combined; rejects openai_chat_completions. - rewrite_bedrock_path: substitutes operator model on both invoke variants; returns None for non-Bedrock paths. - bedrock_route_rewrites_model_in_path_and_preserves_body (wiremock end-to-end): caller sends /model/some-other-model/invoke with a body containing model: "caller-supplied-model-name". Mock asserts the upstream receives /model/<operator-model>/invoke and the body's model field is the caller's value (NOT route.model) — proves both the path rewrite and the body preservation. - bedrock_route_streaming_rewrites_model_in_path: same contract for invoke-with-response-stream. - bedrock_route_rejects_non_bedrock_path: defense-in-depth coverage of the Internal-error path when a Bedrock route receives a path that doesn't match Bedrock shape. Verification (in dev container): - cargo test -p openshell-router --lib: 53/53 (incl. 9 new) - cargo fmt --check: clean - cargo clippy -p openshell-core -p openshell-router -p openshell-server --all-targets -- -D warnings: clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * fix(sandbox/l7): declare framing for Bedrock patterns Closes the buffered-vs-streaming framing warning from gator-agent's re-check on 4ab587f: "Bedrock InvokeModel should be buffered while InvokeModelWithResponseStream is streaming. Please add framing/coverage so /model/{id}/invoke cannot be corrupted by the streaming proxy's truncation/error-frame behavior." The InferenceApiPattern struct gained a `framing: ResponseFraming` field upstream after the original Bedrock-patterns commit (#22b78cff) landed; the cherry-pick onto current upstream/main left the two Bedrock entries without the new field. Fixed here: - aws_bedrock_invoke (POST /model/{id}/invoke): framing = ResponseFraming::Buffered InvokeModel returns one JSON object the caller decodes whole. Sending it through the streaming proxy would risk a mid-body size-cap truncation or idle-timeout failure appending an SSE error event onto bytes the caller decodes as one JSON body — the same corruption mode that drove the existing embeddings + model-discovery to Buffered. - aws_bedrock_invoke_stream (POST /model/{id}/invoke-with-response-stream): framing = ResponseFraming::Streaming InvokeModelWithResponseStream returns an AWS event-stream of binary chunks; the caller wants chunks incrementally, so the streaming proxy path is correct. Two new tests in `crates/openshell-sandbox/src/l7/inference.rs` pin down the contract: - aws_bedrock_invoke_is_buffered — detect_inference_pattern returns a Buffered pattern for /model/<id>/invoke, with explanatory message naming the corruption mode being prevented. - aws_bedrock_invoke_stream_is_streaming — same shape, asserting Streaming for /model/<id>/invoke-with-response-stream. Verification (in dev container): - cargo check -p openshell-sandbox: clean (was failing on missing `framing` field before this commit) - cargo test -p openshell-sandbox --lib l7::inference::tests::aws_bedrock: 7/7 (incl. 2 new framing tests) - cargo fmt --check: clean - cargo clippy --all-targets -- -D warnings: clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * fix(inference): drop aws_bedrock_invoke_stream until protocol-aware errors land Per PR #1704 review (johntmyers): defer Bedrock streaming to a follow-up that also wires protocol-aware error framing for the AWS event-stream shape. Until then, surfacing `/model/{id}/invoke-with-response-stream` risks shipping responses the sandbox cannot interpret on failure. - AWS_BEDROCK_PROTOCOLS no longer advertises `aws_bedrock_invoke_stream`. - The L7 inference pattern table drops the streaming entry; only `aws_bedrock_invoke` (buffered) is recognized. - Test `aws_bedrock_invoke_stream_pattern_is_deferred` asserts no pattern claims that protocol so the gap is visible. Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * fix(inference): validate AWS Bedrock model_id at route + rewrite, preserve query Per PR #1704 review (johntmyers, gator agent): the operator-configured Bedrock model_id flows verbatim into the upstream URL path; the previous plumbing left no enforcement that the value was a single benign path segment, opening a path-injection vector. Defense in depth, both layers: * `openshell-server::inference`: new `validate_aws_bedrock_model_id` rejects empty, leading/trailing whitespace, `/`, `\\`, `?`, `#`, `%`, `..`, control/whitespace characters. Wired into `resolve_provider_route` ahead of base_url resolution so the route store cannot persist a malformed model_id. Mirrors `validate_vertex_model_id` exactly. * `openshell-router::backend`: `rewrite_bedrock_path` now refuses to construct the upstream URL unless `route.model` passes `is_valid_bedrock_model_id`, so even a stale or hand-edited route cannot reach the wire. The parser also drops the `/invoke-with-response-stream` arm to match the protocol catalog. * `parse_bedrock_invocation_path` returns the `?`-prefixed query tail as a third element; `rewrite_bedrock_path` re-attaches it so any caller-supplied query string is preserved through the model rewrite. Tests: 5 unit tests for the validator, 1 integration test that exercises every unsafe-model_id reject path through `upsert_cluster_inference_route`, plus a router-side rewrite-rejects test covering 11 unsafe `route.model` values. All 72 server inference tests + router tests pass. Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * fix(aws-bedrock): make built-in profile non-egress-granting; tighten docs Per PR #1704 review (johntmyers): a single profile that is "usable as is" — not a profile that auto-grants direct AWS Bedrock egress to any sandbox that selects it. The bridge IS the egress point and is operator-managed; the profile must not implicitly punch a hole through the cluster's network policy on its behalf. * `providers/aws-bedrock.yaml`: clear `endpoints` and `binaries` to empty arrays. Rewrite the description to spell out that the profile is intentionally non-egress-granting, that operators are responsible for declaring their bridge's egress endpoint and binary attribution, and that the SigV4 follow-up will repopulate these fields once router-side signing exists. * `docs/sandboxes/inference-routing.mdx`: - Drop the `InvokeModelWithResponseStream` row from the supported patterns table (matches the protocol-catalog + L7-pattern drop). - Update the `--no-verify` paragraph to reference only `aws_bedrock_invoke`. - Generalise the placeholder-credential rationale: any standalone-router profile registering `AuthHeader::None` will hit the same non-empty-credentials structural requirement. Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * style(router): satisfy cargo fmt for Bedrock query-tail extraction CI's rustfmt rejected the one-line `let (path_only, query_tail) = path.find('?').map_or(...)` shape from commit c51160e and required the chained-method layout instead. Functional behaviour is unchanged; tests still pass. Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * style(router): drop unnecessary `crate::` prefix on RouterError matcher `RouterError` is already imported at the top of the file, so the two test-side `matches!(result, Err(crate::RouterError::UpstreamProtocol(_)))` uses trip clippy's `unused_qualifications` under `-D warnings`. Drop the prefix on both sites; functional behaviour is unchanged. These warnings predate this PR (originated in 25abc9e on 2026-06-07) but NVIDIA's `rust:lint` re-runs because this PR touches `backend.rs`, so the lint regression surfaces here. Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> * style(server): silence unused-variable lint on first shutdown_tx `crates/openshell-server/src/lib.rs` declares `(shutdown_tx, shutdown_rx)` twice in `run_server` — the first pair (introduced by upstream #1577 reconciler-lease work) only consumes `shutdown_rx`, leaving the first `shutdown_tx` unused. Clippy's `-D warnings` flags it on workspace lint. Rebasing this Bedrock-scoped PR onto current upstream/main exposes the regression because we re-touch the file and the lint re-runs. Prefix the unused half with an underscore so the workspace lint is clean. Behaviour is unchanged — only the second shutdown_tx (line ~425) ever sends a shutdown signal today. This is a drive-by upstream fix unrelated to the Bedrock provider work; keeping it separate so it can be cherry-picked or reverted independently of the Bedrock commits. Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> --------- Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 48545cf commit 4ee27d9

10 files changed

Lines changed: 1104 additions & 29 deletions

File tree

crates/openshell-core/src/inference.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ pub enum AuthHeader {
1818
Bearer,
1919
/// Custom header name (e.g. `x-api-key` for Anthropic).
2020
Custom(&'static str),
21+
/// Do not inject any auth header on outgoing requests. The upstream
22+
/// is expected to authenticate itself — used when the configured
23+
/// `default_base_url` (or operator-supplied base-URL override) points
24+
/// at a translating bridge / proxy that holds operator-side
25+
/// credentials in its own pod and ignores caller-supplied auth.
26+
/// Currently used by the `aws-bedrock` profile, where `SigV4` signing
27+
/// is deferred to a follow-up PR; today the only supported shape is
28+
/// a bridge-fronted upstream.
29+
None,
2130
}
2231

2332
// ---------------------------------------------------------------------------
@@ -69,6 +78,14 @@ const ANTHROPIC_PROTOCOLS: &[&str] = &["anthropic_messages", "model_discovery"];
6978
/// base-URL-override escape hatch path.
7079
const VERTEX_AI_PROTOCOLS: &[&str] = &["anthropic_messages", "model_discovery"];
7180

81+
// `aws_bedrock_invoke_stream` (`/model/{id}/invoke-with-response-stream`) is
82+
// deferred to a follow-up alongside protocol-aware AWS event-stream error
83+
// handling: the shared streaming relay's truncation/timeout path injects
84+
// SSE-formatted error frames, which would corrupt downstream Bedrock
85+
// event-stream parsers. Until that lands, this profile advertises only
86+
// the buffered `InvokeModel` shape.
87+
const AWS_BEDROCK_PROTOCOLS: &[&str] = &["aws_bedrock_invoke"];
88+
7289
static OPENAI_PROFILE: InferenceProviderProfile = InferenceProviderProfile {
7390
provider_type: "openai",
7491
default_base_url: "https://api.openai.com/v1",
@@ -160,6 +177,38 @@ static DEEPINFRA_PROFILE: InferenceProviderProfile = InferenceProviderProfile {
160177
passthrough_headers: &["x-model-id"],
161178
};
162179

180+
// AWS Bedrock — registered as bridge-fronted (no router-side auth
181+
// injection). Real AWS Bedrock requires `SigV4` signing of every request,
182+
// which is deferred to a follow-up PR (see #1704 thread). Until then,
183+
// operators point `BEDROCK_BASE_URL` at a translating bridge or
184+
// Bedrock-compatible proxy that handles auth in its own pod. The router
185+
// passes Bedrock `InvokeModel` requests through opaquely; the L7 pattern
186+
// `/model/{modelId}/invoke` is wired up in
187+
// `crates/openshell-sandbox/src/l7/inference.rs`. `InvokeModelWithResponseStream`
188+
// is deferred to the same follow-up that adds protocol-aware error framing.
189+
//
190+
// Note: `default_base_url` is intentionally an empty string. Without
191+
// `BEDROCK_BASE_URL` config, route resolution rejects the provider
192+
// rather than silently forwarding prompts to real AWS Bedrock with
193+
// `auth: None` (which would fail upstream and risks operator
194+
// surprise). Once the `SigV4` follow-up lands, the default can revert
195+
// to `https://bedrock-runtime.us-east-1.amazonaws.com`.
196+
static AWS_BEDROCK_PROFILE: InferenceProviderProfile = InferenceProviderProfile {
197+
provider_type: "aws-bedrock",
198+
default_base_url: "",
199+
protocols: AWS_BEDROCK_PROTOCOLS,
200+
// No single API key for Bedrock — `SigV4` takes four credentials
201+
// (access key id, secret, session token, region) and signs requests
202+
// rather than injecting a header. Until the `SigV4` follow-up lands
203+
// the router-side auth shape is `None` and no credential lookup is
204+
// required at route time.
205+
credential_key_names: &[],
206+
base_url_config_keys: &["BEDROCK_BASE_URL"],
207+
auth: AuthHeader::None,
208+
default_headers: &[],
209+
passthrough_headers: &[],
210+
};
211+
163212
/// Canonicalize an inference provider type string to a well-known identifier.
164213
///
165214
/// Returns `Some(canonical_name)` for recognized inference providers,
@@ -173,6 +222,7 @@ pub fn normalize_inference_provider_type(input: &str) -> Option<&'static str> {
173222
"anthropic" => Some("anthropic"),
174223
"nvidia" => Some("nvidia"),
175224
"deepinfra" => Some("deepinfra"),
225+
"aws-bedrock" => Some("aws-bedrock"),
176226
"google-vertex-ai" | "vertex" | "vertex-ai" | "google-vertex" | "gcp-vertex" => {
177227
Some("google-vertex-ai")
178228
}
@@ -191,6 +241,7 @@ pub fn profile_for(provider_type: &str) -> Option<&'static InferenceProviderProf
191241
"nvidia" => Some(&NVIDIA_PROFILE),
192242
"deepinfra" => Some(&DEEPINFRA_PROFILE),
193243
"google-vertex-ai" => Some(&VERTEX_AI_PROFILE),
244+
"aws-bedrock" => Some(&AWS_BEDROCK_PROFILE),
194245
_ => None,
195246
}
196247
}
@@ -311,7 +362,34 @@ mod tests {
311362
assert!(profile_for("anthropic").is_some());
312363
assert!(profile_for("nvidia").is_some());
313364
assert!(profile_for("deepinfra").is_some());
365+
assert!(profile_for("aws-bedrock").is_some());
314366
assert!(profile_for("OpenAI").is_some()); // case insensitive
367+
assert!(profile_for("AWS-Bedrock").is_some()); // case insensitive
368+
}
369+
370+
#[test]
371+
fn aws_bedrock_uses_no_auth_header() {
372+
let (auth, headers) = auth_for_provider_type("aws-bedrock");
373+
assert_eq!(auth, AuthHeader::None);
374+
assert!(headers.is_empty());
375+
}
376+
377+
#[test]
378+
fn aws_bedrock_profile_has_no_credential_keys() {
379+
let profile = profile_for("aws-bedrock").expect("profile registered");
380+
// No router-side credential lookup until the `SigV4` follow-up.
381+
assert!(profile.credential_key_names.is_empty());
382+
assert_eq!(profile.base_url_config_keys, &["BEDROCK_BASE_URL"]);
383+
}
384+
385+
#[test]
386+
fn aws_bedrock_protocols_are_bedrock_specific() {
387+
let profile = profile_for("aws-bedrock").expect("profile registered");
388+
assert!(profile.protocols.contains(&"aws_bedrock_invoke"));
389+
// `aws_bedrock_invoke_stream` is deferred to the follow-up that adds
390+
// protocol-aware AWS event-stream error framing; until then the
391+
// profile advertises only the buffered `InvokeModel` shape.
392+
assert!(!profile.protocols.contains(&"aws_bedrock_invoke_stream"));
315393
}
316394

317395
#[test]

crates/openshell-providers/src/profiles.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use std::sync::OnceLock;
2020
const PATH_TEMPLATE_CREDENTIAL_PLACEHOLDER: &str = "{credential}";
2121

2222
const BUILT_IN_PROFILE_YAMLS: &[&str] = &[
23+
include_str!("../../../providers/aws-bedrock.yaml"),
2324
include_str!("../../../providers/claude-code.yaml"),
2425
include_str!("../../../providers/codex.yaml"),
2526
include_str!("../../../providers/copilot.yaml"),

0 commit comments

Comments
 (0)