Skip to content
68 changes: 68 additions & 0 deletions crates/openshell-core/src/inference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -69,6 +78,8 @@ const ANTHROPIC_PROTOCOLS: &[&str] = &["anthropic_messages", "model_discovery"];
/// base-URL-override escape hatch path.
const VERTEX_AI_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",
Expand Down Expand Up @@ -155,6 +166,37 @@ 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`.
//
// Note: `default_base_url` is intentionally an empty string. Without
// `BEDROCK_BASE_URL` config, route resolution rejects the provider
// rather than silently forwarding prompts to real AWS Bedrock with
// `auth: None` (which would fail upstream and risks operator
// surprise). Once the `SigV4` follow-up lands, the default can revert
// to `https://bedrock-runtime.us-east-1.amazonaws.com`.
static AWS_BEDROCK_PROFILE: InferenceProviderProfile = InferenceProviderProfile {
provider_type: "aws-bedrock",
default_base_url: "",
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: &[],
};

/// Canonicalize an inference provider type string to a well-known identifier.
///
/// Returns `Some(canonical_name)` for recognized inference providers,
Expand All @@ -167,6 +209,7 @@ pub fn normalize_inference_provider_type(input: &str) -> Option<&'static str> {
"openai" => Some("openai"),
"anthropic" => Some("anthropic"),
"nvidia" => Some("nvidia"),
"aws-bedrock" => Some("aws-bedrock"),
"google-vertex-ai" | "vertex" | "vertex-ai" | "google-vertex" | "gcp-vertex" => {
Some("google-vertex-ai")
}
Expand All @@ -184,6 +227,7 @@ pub fn profile_for(provider_type: &str) -> Option<&'static InferenceProviderProf
"anthropic" => Some(&ANTHROPIC_PROFILE),
"nvidia" => Some(&NVIDIA_PROFILE),
"google-vertex-ai" => Some(&VERTEX_AI_PROFILE),
"aws-bedrock" => Some(&AWS_BEDROCK_PROFILE),
_ => None,
}
}
Expand Down Expand Up @@ -303,7 +347,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]
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-providers/src/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::sync::OnceLock;
const PATH_TEMPLATE_CREDENTIAL_PLACEHOLDER: &str = "{credential}";

const BUILT_IN_PROFILE_YAMLS: &[&str] = &[
include_str!("../../../providers/aws-bedrock.yaml"),
include_str!("../../../providers/claude-code.yaml"),
include_str!("../../../providers/codex.yaml"),
include_str!("../../../providers/copilot.yaml"),
Expand Down
Loading
Loading