From 8b30211aa959189b88b903490d7470997e351904 Mon Sep 17 00:00:00 2001 From: st-gr <38470677+st-gr@users.noreply.github.com> Date: Mon, 1 Jun 2026 05:02:31 -0700 Subject: [PATCH 1/5] feat(sandbox): allow AWS Bedrock InvokeModel paths through the L7 router MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> --- crates/openshell-sandbox/src/l7/inference.rs | 98 +++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/crates/openshell-sandbox/src/l7/inference.rs b/crates/openshell-sandbox/src/l7/inference.rs index acda0bb36..8dee88919 100644 --- a/crates/openshell-sandbox/src/l7/inference.rs +++ b/crates/openshell-sandbox/src/l7/inference.rs @@ -16,7 +16,7 @@ pub struct InferenceApiPattern { pub kind: String, } -/// Default patterns for known inference APIs (`OpenAI`, Anthropic). +/// Default patterns for known inference APIs (`OpenAI`, Anthropic, AWS Bedrock). pub fn default_patterns() -> Vec { vec![ InferenceApiPattern { @@ -55,10 +55,31 @@ pub fn default_patterns() -> Vec { protocol: "model_discovery".to_string(), kind: "models_get".to_string(), }, + // AWS Bedrock InvokeModel + InvokeModelWithResponseStream. The `*` + // segment is the Bedrock model id (e.g. `anthropic.claude-opus-4-7`). + InferenceApiPattern { + method: "POST".to_string(), + path_glob: "/model/*/invoke".to_string(), + protocol: "aws_bedrock_invoke".to_string(), + kind: "messages".to_string(), + }, + InferenceApiPattern { + method: "POST".to_string(), + path_glob: "/model/*/invoke-with-response-stream".to_string(), + protocol: "aws_bedrock_invoke_stream".to_string(), + kind: "messages".to_string(), + }, ] } /// Check if an HTTP request matches a known inference API pattern. +/// +/// Path globs support two wildcard shapes (one per pattern, not both): +/// - **Trailing `/*`**: `/v1/models/*` matches `/v1/models` and any +/// `/v1/models/` (one or many path segments). +/// - **Middle `/*/`**: `/model/*/invoke` matches `/model//invoke` +/// for a single non-empty segment that contains no `/`. Used for +/// AWS Bedrock's `/model/{modelId}/invoke[-with-response-stream]`. pub fn detect_inference_pattern<'a>( method: &str, path: &str, @@ -78,6 +99,21 @@ pub fn detect_inference_pattern<'a>( .is_some_and(|suffix| suffix.starts_with('/')); } + if let Some((before, after)) = p.path_glob.split_once("/*/") { + let Some(rest) = path_only.strip_prefix(before) else { + return false; + }; + let Some(rest) = rest.strip_prefix('/') else { + return false; + }; + // rest must look like `/` where is non-empty + // and contains no `/` (single path segment). + let Some(slash_at) = rest.find('/') else { + return false; + }; + return slash_at > 0 && rest[slash_at + 1..] == *after; + } + path_only == p.path_glob }) } @@ -445,6 +481,66 @@ mod tests { assert!(result.is_none()); } + #[test] + fn detect_aws_bedrock_invoke() { + let patterns = default_patterns(); + let result = + detect_inference_pattern("POST", "/model/anthropic.claude-opus-4-7/invoke", &patterns); + assert!(result.is_some()); + assert_eq!(result.unwrap().protocol, "aws_bedrock_invoke"); + assert_eq!(result.unwrap().kind, "messages"); + } + + #[test] + fn detect_aws_bedrock_invoke_stream() { + let patterns = default_patterns(); + let result = detect_inference_pattern( + "POST", + "/model/anthropic.claude-opus-4-7/invoke-with-response-stream", + &patterns, + ); + assert!(result.is_some()); + assert_eq!(result.unwrap().protocol, "aws_bedrock_invoke_stream"); + } + + #[test] + fn aws_bedrock_invoke_with_query_string() { + let patterns = default_patterns(); + let result = detect_inference_pattern("POST", "/model/foo.bar/invoke?trace=1", &patterns); + assert!(result.is_some()); + assert_eq!(result.unwrap().protocol, "aws_bedrock_invoke"); + } + + #[test] + fn aws_bedrock_rejects_empty_model_id() { + let patterns = default_patterns(); + // `/model//invoke` — empty wildcard segment is not a valid Bedrock id. + assert!(detect_inference_pattern("POST", "/model//invoke", &patterns).is_none()); + } + + #[test] + fn aws_bedrock_rejects_multi_segment_model_id() { + let patterns = default_patterns(); + // The `*` matches a single path segment only; multi-segment ids must + // not match (would be a path-traversal liability otherwise). + assert!(detect_inference_pattern("POST", "/model/foo/bar/invoke", &patterns).is_none()); + } + + #[test] + fn aws_bedrock_rejects_get() { + let patterns = default_patterns(); + assert!( + detect_inference_pattern("GET", "/model/anthropic.claude-opus-4-7/invoke", &patterns) + .is_none() + ); + } + + #[test] + fn aws_bedrock_rejects_unknown_action() { + let patterns = default_patterns(); + assert!(detect_inference_pattern("POST", "/model/foo/converse", &patterns).is_none()); + } + #[test] fn parse_simple_post_request() { let body = b"{\"hello\":true}"; From 6b51e1a63c10d90c014d099c047262113aacf466 Mon Sep 17 00:00:00 2001 From: st-gr <38470677+st-gr@users.noreply.github.com> Date: Mon, 1 Jun 2026 05:03:09 -0700 Subject: [PATCH 2/5] 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) Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> --- crates/openshell-providers/src/lib.rs | 1 + crates/openshell-providers/src/profiles.rs | 1 + .../src/providers/aws_bedrock.rs | 20 ++++++++++ .../openshell-providers/src/providers/mod.rs | 1 + providers/aws-bedrock.yaml | 38 +++++++++++++++++++ 5 files changed, 61 insertions(+) create mode 100644 crates/openshell-providers/src/providers/aws_bedrock.rs create mode 100644 providers/aws-bedrock.yaml diff --git a/crates/openshell-providers/src/lib.rs b/crates/openshell-providers/src/lib.rs index 21a1750ab..3f5d03b00 100644 --- a/crates/openshell-providers/src/lib.rs +++ b/crates/openshell-providers/src/lib.rs @@ -115,6 +115,7 @@ impl ProviderRegistry { registry.register(providers::generic::GenericProvider); registry.register(providers::openai::SPEC); registry.register(providers::anthropic::SPEC); + registry.register(providers::aws_bedrock::SPEC); registry.register(providers::nvidia::SPEC); registry.register(providers::gitlab::SPEC); registry.register(providers::github::SPEC); diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 68cc06260..8c51c4796 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -17,6 +17,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::OnceLock; const BUILT_IN_PROFILE_YAMLS: &[&str] = &[ + include_str!("../../../providers/aws-bedrock.yaml"), include_str!("../../../providers/claude-code.yaml"), include_str!("../../../providers/github.yaml"), include_str!("../../../providers/nvidia.yaml"), diff --git a/crates/openshell-providers/src/providers/aws_bedrock.rs b/crates/openshell-providers/src/providers/aws_bedrock.rs new file mode 100644 index 000000000..d696774f8 --- /dev/null +++ b/crates/openshell-providers/src/providers/aws_bedrock.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::ProviderDiscoverySpec; + +pub const SPEC: ProviderDiscoverySpec = ProviderDiscoverySpec { + id: "aws-bedrock", + credential_env_vars: &[ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + "AWS_REGION", + ], +}; + +test_discovers_env_credential!( + discovers_aws_bedrock_env_credentials, + "AWS_ACCESS_KEY_ID", + "AKIA-test-key" +); diff --git a/crates/openshell-providers/src/providers/mod.rs b/crates/openshell-providers/src/providers/mod.rs index dfe5935a1..57a8f2053 100644 --- a/crates/openshell-providers/src/providers/mod.rs +++ b/crates/openshell-providers/src/providers/mod.rs @@ -31,6 +31,7 @@ macro_rules! test_discovers_env_credential { }; } pub mod anthropic; +pub mod aws_bedrock; pub mod claude; pub mod codex; pub mod copilot; diff --git a/providers/aws-bedrock.yaml b/providers/aws-bedrock.yaml new file mode 100644 index 000000000..90bdcce1d --- /dev/null +++ b/providers/aws-bedrock.yaml @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +id: aws-bedrock +display_name: AWS Bedrock +description: Anthropic + Mistral + Llama models served via the AWS Bedrock InvokeModel API +category: inference +inference_capable: true +credentials: + - name: aws_access_key_id + description: AWS access key id used for SigV4 signing of outbound Bedrock requests + env_vars: [AWS_ACCESS_KEY_ID] + required: true + - name: aws_secret_access_key + description: AWS secret access key paired with aws_access_key_id + env_vars: [AWS_SECRET_ACCESS_KEY] + required: true + - name: aws_session_token + description: Optional session token for temporary credentials (STS, IAM Roles for Service Accounts) + env_vars: [AWS_SESSION_TOKEN] + required: false + - name: aws_region + description: AWS region the Bedrock endpoint resolves into (e.g. us-east-1) + env_vars: [AWS_REGION, AWS_DEFAULT_REGION] + required: true +discovery: + credentials: [aws_access_key_id, aws_secret_access_key, aws_region] +endpoints: + # Default endpoint targets us-east-1 since the YAML loader does not yet + # substitute the `{region}` placeholder. Operators in other regions + # override via the `BEDROCK_BASE_URL` config-key the same way the + # `anthropic` provider accepts `ANTHROPIC_BASE_URL`. + - host: bedrock-runtime.us-east-1.amazonaws.com + port: 443 + protocol: rest + access: read-write + enforcement: enforce +binaries: [/usr/bin/claude, /usr/local/bin/claude] From 508a4e6c1fb88c7c1550f2cebb50c24301116db6 Mon Sep 17 00:00:00 2001 From: st-gr <38470677+st-gr@users.noreply.github.com> Date: Fri, 5 Jun 2026 01:09:16 -0700 Subject: [PATCH 3/5] revert(providers): drop legacy aws-bedrock SPEC, rely on v2 YAML profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses johntmyers's review on NVIDIA/OpenShell#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) Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> --- crates/openshell-providers/src/lib.rs | 1 - .../src/providers/aws_bedrock.rs | 20 ------------------- .../openshell-providers/src/providers/mod.rs | 1 - 3 files changed, 22 deletions(-) delete mode 100644 crates/openshell-providers/src/providers/aws_bedrock.rs diff --git a/crates/openshell-providers/src/lib.rs b/crates/openshell-providers/src/lib.rs index 3f5d03b00..21a1750ab 100644 --- a/crates/openshell-providers/src/lib.rs +++ b/crates/openshell-providers/src/lib.rs @@ -115,7 +115,6 @@ impl ProviderRegistry { registry.register(providers::generic::GenericProvider); registry.register(providers::openai::SPEC); registry.register(providers::anthropic::SPEC); - registry.register(providers::aws_bedrock::SPEC); registry.register(providers::nvidia::SPEC); registry.register(providers::gitlab::SPEC); registry.register(providers::github::SPEC); diff --git a/crates/openshell-providers/src/providers/aws_bedrock.rs b/crates/openshell-providers/src/providers/aws_bedrock.rs deleted file mode 100644 index d696774f8..000000000 --- a/crates/openshell-providers/src/providers/aws_bedrock.rs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -use crate::ProviderDiscoverySpec; - -pub const SPEC: ProviderDiscoverySpec = ProviderDiscoverySpec { - id: "aws-bedrock", - credential_env_vars: &[ - "AWS_ACCESS_KEY_ID", - "AWS_SECRET_ACCESS_KEY", - "AWS_SESSION_TOKEN", - "AWS_REGION", - ], -}; - -test_discovers_env_credential!( - discovers_aws_bedrock_env_credentials, - "AWS_ACCESS_KEY_ID", - "AKIA-test-key" -); diff --git a/crates/openshell-providers/src/providers/mod.rs b/crates/openshell-providers/src/providers/mod.rs index 57a8f2053..dfe5935a1 100644 --- a/crates/openshell-providers/src/providers/mod.rs +++ b/crates/openshell-providers/src/providers/mod.rs @@ -31,7 +31,6 @@ macro_rules! test_discovers_env_credential { }; } pub mod anthropic; -pub mod aws_bedrock; pub mod claude; pub mod codex; pub mod copilot; From 3d0ad1c6380980225b7af8df526674d8711047d6 Mon Sep 17 00:00:00 2001 From: st-gr <38470677+st-gr@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:28:53 -0700 Subject: [PATCH 4/5] fix(providers): include aws_session_token in discovery + update profile assertion Two fixes from johntmyers's gator-agent re-check on NVIDIA/OpenShell#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 Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> --- crates/openshell-server/src/grpc/provider.rs | 2 +- providers/aws-bedrock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index c58c0c9b7..30901ac7f 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -1617,7 +1617,7 @@ mod tests { .iter() .map(|profile| profile.id.as_str()) .collect::>(); - assert_eq!(ids, vec!["claude-code", "github", "nvidia"]); + assert_eq!(ids, vec!["aws-bedrock", "claude-code", "github", "nvidia"]); let github = response .profiles diff --git a/providers/aws-bedrock.yaml b/providers/aws-bedrock.yaml index 90bdcce1d..01a51b9e8 100644 --- a/providers/aws-bedrock.yaml +++ b/providers/aws-bedrock.yaml @@ -24,7 +24,7 @@ credentials: env_vars: [AWS_REGION, AWS_DEFAULT_REGION] required: true discovery: - credentials: [aws_access_key_id, aws_secret_access_key, aws_region] + credentials: [aws_access_key_id, aws_secret_access_key, aws_session_token, aws_region] endpoints: # Default endpoint targets us-east-1 since the YAML loader does not yet # substitute the `{region}` placeholder. Operators in other regions From 4ab587f1bdab2c2565462fe5ed9cc6bb3a5d0c12 Mon Sep 17 00:00:00 2001 From: st-gr <38470677+st-gr@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:23:01 -0700 Subject: [PATCH 5/5] feat(inference): register aws-bedrock profile (bridge-fronted) + docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 (8b30211a) and the YAML profile (6b51e1a6) 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 Signed-off-by: st-gr <38470677+st-gr@users.noreply.github.com> --- crates/openshell-core/src/inference.rs | 60 ++++++++++++++++++ crates/openshell-router/src/backend.rs | 7 +++ crates/openshell-server/src/inference.rs | 78 +++++++++++++++++++++++- docs/sandboxes/inference-routing.mdx | 32 +++++++++- docs/sandboxes/manage-providers.mdx | 2 + 5 files changed, 175 insertions(+), 4 deletions(-) diff --git a/crates/openshell-core/src/inference.rs b/crates/openshell-core/src/inference.rs index 0360cae5c..37fde7290 100644 --- a/crates/openshell-core/src/inference.rs +++ b/crates/openshell-core/src/inference.rs @@ -18,6 +18,15 @@ pub enum AuthHeader { Bearer, /// Custom header name (e.g. `x-api-key` for Anthropic). Custom(&'static str), + /// Do not inject any auth header on outgoing requests. The upstream + /// is expected to authenticate itself — used when the configured + /// `default_base_url` (or operator-supplied base-URL override) points + /// at a translating bridge / proxy that holds operator-side + /// credentials in its own pod and ignores caller-supplied auth. + /// Currently used by the `aws-bedrock` profile, where `SigV4` signing + /// is deferred to a follow-up PR; today the only supported shape is + /// a bridge-fronted upstream. + None, } // --------------------------------------------------------------------------- @@ -61,6 +70,8 @@ const OPENAI_PROTOCOLS: &[&str] = &[ const ANTHROPIC_PROTOCOLS: &[&str] = &["anthropic_messages", "model_discovery"]; +const AWS_BEDROCK_PROTOCOLS: &[&str] = &["aws_bedrock_invoke", "aws_bedrock_invoke_stream"]; + static OPENAI_PROFILE: InferenceProviderProfile = InferenceProviderProfile { provider_type: "openai", default_base_url: "https://api.openai.com/v1", @@ -94,6 +105,30 @@ static NVIDIA_PROFILE: InferenceProviderProfile = InferenceProviderProfile { passthrough_headers: &["x-model-id"], }; +// AWS Bedrock — registered as bridge-fronted (no router-side auth +// injection). Real AWS Bedrock requires `SigV4` signing of every request, +// which is deferred to a follow-up PR (see #1704 thread). Until then, +// operators point `BEDROCK_BASE_URL` at a translating bridge or +// Bedrock-compatible proxy that handles auth in its own pod. The router +// passes Bedrock InvokeModel requests through opaquely; the L7 patterns +// `/model/{modelId}/invoke` and `/model/{modelId}/invoke-with-response-stream` +// are wired up in `crates/openshell-sandbox/src/l7/inference.rs`. +static AWS_BEDROCK_PROFILE: InferenceProviderProfile = InferenceProviderProfile { + provider_type: "aws-bedrock", + default_base_url: "https://bedrock-runtime.us-east-1.amazonaws.com", + protocols: AWS_BEDROCK_PROTOCOLS, + // No single API key for Bedrock — `SigV4` takes four credentials + // (access key id, secret, session token, region) and signs requests + // rather than injecting a header. Until the `SigV4` follow-up lands + // the router-side auth shape is `None` and no credential lookup is + // required at route time. + credential_key_names: &[], + base_url_config_keys: &["BEDROCK_BASE_URL"], + auth: AuthHeader::None, + default_headers: &[], + passthrough_headers: &[], +}; + /// Look up the inference provider profile for a given provider type. /// /// Returns `None` for provider types that don't support inference routing @@ -103,6 +138,7 @@ pub fn profile_for(provider_type: &str) -> Option<&'static InferenceProviderProf "openai" => Some(&OPENAI_PROFILE), "anthropic" => Some(&ANTHROPIC_PROFILE), "nvidia" => Some(&NVIDIA_PROFILE), + "aws-bedrock" => Some(&AWS_BEDROCK_PROFILE), _ => None, } } @@ -200,7 +236,31 @@ mod tests { assert!(profile_for("openai").is_some()); assert!(profile_for("anthropic").is_some()); assert!(profile_for("nvidia").is_some()); + assert!(profile_for("aws-bedrock").is_some()); assert!(profile_for("OpenAI").is_some()); // case insensitive + assert!(profile_for("AWS-Bedrock").is_some()); // case insensitive + } + + #[test] + fn aws_bedrock_uses_no_auth_header() { + let (auth, headers) = auth_for_provider_type("aws-bedrock"); + assert_eq!(auth, AuthHeader::None); + assert!(headers.is_empty()); + } + + #[test] + fn aws_bedrock_profile_has_no_credential_keys() { + let profile = profile_for("aws-bedrock").expect("profile registered"); + // No router-side credential lookup until the `SigV4` follow-up. + assert!(profile.credential_key_names.is_empty()); + assert_eq!(profile.base_url_config_keys, &["BEDROCK_BASE_URL"]); + } + + #[test] + fn aws_bedrock_protocols_are_bedrock_specific() { + let profile = profile_for("aws-bedrock").expect("profile registered"); + assert!(profile.protocols.contains(&"aws_bedrock_invoke")); + assert!(profile.protocols.contains(&"aws_bedrock_invoke_stream")); } #[test] diff --git a/crates/openshell-router/src/backend.rs b/crates/openshell-router/src/backend.rs index 88a6e213a..66e41cd6e 100644 --- a/crates/openshell-router/src/backend.rs +++ b/crates/openshell-router/src/backend.rs @@ -174,6 +174,13 @@ fn prepare_backend_request( AuthHeader::Custom(header_name) => { builder = builder.header(*header_name, &route.api_key); } + AuthHeader::None => { + // Bridge-fronted upstream: no router-side auth injection. + // The configured `endpoint` is expected to be a translating + // bridge / proxy whose own pod holds operator-side + // credentials. Used today by the `aws-bedrock` profile + // (SigV4 signing is a separate follow-up). + } } for (name, value) in &headers { builder = builder.header(name.as_str(), value.as_str()); diff --git a/crates/openshell-server/src/inference.rs b/crates/openshell-server/src/inference.rs index f393caab3..2e426a1a6 100644 --- a/crates/openshell-server/src/inference.rs +++ b/crates/openshell-server/src/inference.rs @@ -265,18 +265,24 @@ fn resolve_provider_route(provider: &Provider) -> Result + + + +| Pattern | Method | Path | +|---|---|---| +| InvokeModel | `POST` | `/model/{modelId}/invoke` | +| InvokeModelWithResponseStream | `POST` | `/model/{modelId}/invoke-with-response-stream` | + +The `{modelId}` segment is constrained to a single non-empty path segment to avoid path-traversal liabilities. `/model//invoke` and `/model/a/b/invoke` both no-match. + + +Today the `aws-bedrock` provider type is bridge-fronted only. The router does not inject any auth header on outbound requests; the configured `BEDROCK_BASE_URL` is expected to point at a translating bridge or Bedrock-compatible proxy whose own pod holds operator-side credentials. SigV4 signing for direct AWS Bedrock is deferred to a follow-up release. + + @@ -130,6 +145,21 @@ openshell provider create --name anthropic-prod --type anthropic --from-existing This reads `ANTHROPIC_API_KEY` from your environment. + + + + +```shell +openshell provider create \ + --name bedrock-bridge \ + --type aws-bedrock \ + --config BEDROCK_BASE_URL=http://your-bedrock-bridge.your-ns.svc.cluster.local:8080 +``` + +The `aws-bedrock` provider type is bridge-fronted: the router does not inject any auth header on outbound requests. Point `BEDROCK_BASE_URL` at a translating bridge or Bedrock-compatible proxy that handles authentication in its own pod. The bridge is expected to accept Bedrock InvokeModel requests on the patterns listed above and forward to the operator's real upstream. + +For direct AWS Bedrock with SigV4 signing, refer to a future release that adds the SigV4 router-side signer. Today the discovery scan picks up `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, and `AWS_REGION` from your environment via `--from-existing`, but the router does not yet sign requests with them. + diff --git a/docs/sandboxes/manage-providers.mdx b/docs/sandboxes/manage-providers.mdx index a923e9077..aaff26f5a 100644 --- a/docs/sandboxes/manage-providers.mdx +++ b/docs/sandboxes/manage-providers.mdx @@ -245,6 +245,7 @@ The following provider types are supported. | Type | Environment Variables Injected | Typical Use | |---|---|---| | `anthropic` | `ANTHROPIC_API_KEY` | Anthropic API | +| `aws-bedrock` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION` | AWS Bedrock InvokeModel via a translating bridge. Today the router does not inject any auth header; the configured `BEDROCK_BASE_URL` upstream is expected to handle auth itself. Refer to [Inference Routing](/sandboxes/inference-routing). | | `claude` | `ANTHROPIC_API_KEY`, `CLAUDE_API_KEY` | Claude Code, Anthropic API | | `codex` | `OPENAI_API_KEY` | OpenAI Codex | | `copilot` | `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN` | GitHub Copilot CLI | @@ -271,6 +272,7 @@ The following providers have been tested with `inference.local`. Any provider th | Provider | Name | Type | Base URL | API Key Variable | |---|---|---|---|---| +| AWS Bedrock (via bridge) | `bedrock-bridge` | `aws-bedrock` | Operator-supplied `BEDROCK_BASE_URL` | None at router level (bridge holds creds) | | NVIDIA API Catalog | `nvidia-prod` | `nvidia` | `https://integrate.api.nvidia.com/v1` | `NVIDIA_API_KEY` | | Anthropic | `anthropic-prod` | `anthropic` | `https://api.anthropic.com` | `ANTHROPIC_API_KEY` | | Baseten | `baseten` | `openai` | `https://inference.baseten.co/v1` | `OPENAI_API_KEY` |