mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-13 15:50:02 +00:00
Support Bedrock API keys (bearer auth)
New optional field: paste the console-generated key, no CLI/IAM setup needed. Takes precedence over SigV4 credentials, matching boto3; live-tested on Converse.
This commit is contained in:
@@ -12,8 +12,12 @@ An id with no family segment falls back to Converse as-is — Converse serves ev
|
||||
model (including Claude, minus the native extras), so a raw model id pasted without the
|
||||
add-model dropdown still works.
|
||||
|
||||
Credentials resolve explicit → named profile → ambient (env / `~/.aws` default / role),
|
||||
matching what AWS CLI users expect; `aws sso login` sessions arrive via the named profile.
|
||||
Credentials, in order:
|
||||
1. A **Bedrock API key** (bearer token from the Bedrock console — the no-CLI path). When
|
||||
present (field or `AWS_BEARER_TOKEN_BEDROCK` env) it WINS over SigV4 credentials, the
|
||||
same precedence boto3 applies; mixing both makes `AnthropicBedrock` raise outright.
|
||||
2. Explicit IAM keys → named profile (covers `aws sso login`) → ambient chain.
|
||||
|
||||
boto3 is a lazy import (packaged via the `bedrock` extra) and returns PLAIN DICTS — every
|
||||
response/stream mapping here is dict-shaped, unlike the attribute objects other SDKs return.
|
||||
"""
|
||||
@@ -22,6 +26,7 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
@@ -260,6 +265,7 @@ class _BedrockConverseClient(ProviderClient):
|
||||
client: Any = None,
|
||||
*,
|
||||
region: Optional[str] = None,
|
||||
bedrock_api_key: Optional[str] = None,
|
||||
profile_name: Optional[str] = None,
|
||||
access_key_id: Optional[str] = None,
|
||||
secret_access_key: Optional[str] = None,
|
||||
@@ -267,6 +273,7 @@ class _BedrockConverseClient(ProviderClient):
|
||||
):
|
||||
self._client = client # tests inject a dict-returning fake
|
||||
self._region = region
|
||||
self._bedrock_api_key = bedrock_api_key
|
||||
self._session_kwargs = _session_kwargs(
|
||||
profile_name, access_key_id, secret_access_key, session_token
|
||||
)
|
||||
@@ -280,6 +287,11 @@ class _BedrockConverseClient(ProviderClient):
|
||||
"AWS Bedrock support needs the boto3 package — "
|
||||
"install with `pip install 'openworker[bedrock]'`."
|
||||
) from exc
|
||||
# boto3 has no per-client bearer parameter — it only reads the env var, and
|
||||
# prefers bearer auth for Bedrock whenever it's set. The sidecar process is
|
||||
# ours, so publishing the configured key there is the supported path.
|
||||
if self._bedrock_api_key:
|
||||
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = self._bedrock_api_key
|
||||
session = boto3.session.Session(**self._session_kwargs)
|
||||
self._client = session.client("bedrock-runtime", region_name=self._region)
|
||||
return self._client
|
||||
@@ -437,6 +449,7 @@ class BedrockProvider(ProviderClient):
|
||||
self,
|
||||
*,
|
||||
region: Optional[str] = None,
|
||||
bedrock_api_key: Optional[str] = None,
|
||||
profile_name: Optional[str] = None,
|
||||
access_key_id: Optional[str] = None,
|
||||
secret_access_key: Optional[str] = None,
|
||||
@@ -445,6 +458,7 @@ class BedrockProvider(ProviderClient):
|
||||
converse_client: Optional[ProviderClient] = None,
|
||||
):
|
||||
self._region = region
|
||||
self._bedrock_api_key = bedrock_api_key
|
||||
self._profile_name = profile_name
|
||||
self._access_key_id = access_key_id
|
||||
self._secret_access_key = secret_access_key
|
||||
@@ -472,18 +486,26 @@ class BedrockProvider(ProviderClient):
|
||||
if family == "claude":
|
||||
from anthropic import AnthropicBedrock
|
||||
|
||||
client = AnthropicProvider(
|
||||
client=AnthropicBedrock(
|
||||
# A Bedrock API key (field or ambient env) takes the bearer path and
|
||||
# EXCLUDES the SigV4 params — AnthropicBedrock raises on a mix.
|
||||
bearer = self._bedrock_api_key or os.environ.get(
|
||||
"AWS_BEARER_TOKEN_BEDROCK"
|
||||
)
|
||||
if bearer:
|
||||
sdk = AnthropicBedrock(api_key=bearer, aws_region=self._region)
|
||||
else:
|
||||
sdk = AnthropicBedrock(
|
||||
aws_region=self._region,
|
||||
aws_profile=self._profile_name,
|
||||
aws_access_key=self._access_key_id,
|
||||
aws_secret_key=self._secret_access_key,
|
||||
aws_session_token=self._session_token,
|
||||
)
|
||||
)
|
||||
client = AnthropicProvider(client=sdk)
|
||||
else:
|
||||
client = _BedrockConverseClient(
|
||||
region=self._region,
|
||||
bedrock_api_key=self._bedrock_api_key,
|
||||
profile_name=self._profile_name,
|
||||
access_key_id=self._access_key_id,
|
||||
secret_access_key=self._secret_access_key,
|
||||
|
||||
@@ -140,6 +140,7 @@ def _build_bedrock(profile: dict[str, Any], secrets: Any) -> ProviderClient:
|
||||
|
||||
return BedrockProvider(
|
||||
region=get("region"),
|
||||
bedrock_api_key=get("bedrock_api_key"),
|
||||
profile_name=get("aws_profile"),
|
||||
access_key_id=get("aws_access_key_id"),
|
||||
secret_access_key=get("aws_secret_access_key"),
|
||||
@@ -299,6 +300,15 @@ DESCRIPTORS: list[ProviderDescriptor] = [
|
||||
placeholder="us-east-1",
|
||||
help="The region your Bedrock model access is enabled in.",
|
||||
),
|
||||
ProviderField(
|
||||
"bedrock_api_key",
|
||||
"Bedrock API key (optional)",
|
||||
secret=True,
|
||||
required=False,
|
||||
placeholder="ABSK…",
|
||||
help="The easiest way in: generate one on the Bedrock console — no AWS "
|
||||
"CLI or IAM setup needed. Takes precedence over the fields below.",
|
||||
),
|
||||
ProviderField(
|
||||
"aws_profile",
|
||||
"AWS profile (optional)",
|
||||
@@ -551,6 +561,10 @@ def _verify_bedrock(fields: dict[str, Any], timeout: float) -> dict[str, Any]:
|
||||
"error": "boto3 is not installed — `pip install 'openworker[bedrock]'`.",
|
||||
}
|
||||
try:
|
||||
# A Bedrock API key rides the env var (boto3's only bearer channel) and then
|
||||
# wins over any SigV4 credentials, matching the provider's own precedence.
|
||||
if get("bedrock_api_key"):
|
||||
os.environ["AWS_BEARER_TOKEN_BEDROCK"] = get("bedrock_api_key")
|
||||
session = boto3.session.Session(
|
||||
**_session_kwargs(
|
||||
get("aws_profile"),
|
||||
|
||||
@@ -21,6 +21,7 @@ export const KEY_HELP: Record<string, { url: string; label: string }> = {
|
||||
openai: { url: "https://platform.openai.com/api-keys", label: "platform.openai.com" },
|
||||
gemini: { url: "https://aistudio.google.com/apikey", label: "aistudio.google.com" },
|
||||
openrouter: { url: "https://openrouter.ai/keys", label: "openrouter.ai" },
|
||||
bedrock: { url: "https://console.aws.amazon.com/bedrock/home#/api-keys", label: "the AWS Bedrock console" },
|
||||
fireworks: { url: "https://fireworks.ai/account/api-keys", label: "fireworks.ai" },
|
||||
together: { url: "https://api.together.xyz/settings/api-keys", label: "together.xyz" },
|
||||
zai: { url: "https://z.ai/manage-apikey/apikey-list", label: "z.ai" },
|
||||
|
||||
@@ -288,17 +288,52 @@ def test_family_dispatch():
|
||||
]
|
||||
|
||||
|
||||
def test_claude_family_builds_native_anthropic_over_bedrock():
|
||||
def test_claude_family_builds_native_anthropic_over_bedrock(monkeypatch):
|
||||
from anthropic import AnthropicBedrock
|
||||
|
||||
from coworker.providers import AnthropicProvider
|
||||
|
||||
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||
p = BedrockProvider(region="us-east-1", profile_name="work")
|
||||
sub = p._family_client("claude")
|
||||
assert isinstance(sub, AnthropicProvider)
|
||||
assert isinstance(sub._client, AnthropicBedrock)
|
||||
|
||||
|
||||
def test_claude_family_prefers_bedrock_api_key_over_sigv4(monkeypatch):
|
||||
"""A Bedrock API key must take the bearer path WITHOUT the SigV4 params —
|
||||
AnthropicBedrock raises outright when both are passed."""
|
||||
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||
p = BedrockProvider(
|
||||
region="us-east-1", bedrock_api_key="ABSKtest", profile_name="work"
|
||||
)
|
||||
sub = p._family_client("claude")
|
||||
assert sub._client.api_key == "ABSKtest"
|
||||
assert sub._client.aws_profile is None
|
||||
|
||||
|
||||
def test_converse_client_publishes_api_key_as_bearer_env(monkeypatch):
|
||||
import os
|
||||
|
||||
import boto3
|
||||
|
||||
from coworker.providers.bedrock_provider import _BedrockConverseClient
|
||||
|
||||
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||
|
||||
class _FakeSession:
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
def client(self, service, **kwargs):
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr(boto3.session, "Session", _FakeSession)
|
||||
client = _BedrockConverseClient(region="us-east-1", bedrock_api_key="ABSKtest")
|
||||
client._ensure_client()
|
||||
assert os.environ["AWS_BEARER_TOKEN_BEDROCK"] == "ABSKtest"
|
||||
|
||||
|
||||
# -- capabilities / matrix ------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -334,6 +369,7 @@ def test_bedrock_descriptor_and_builder():
|
||||
keys = [f.key for f in d.fields]
|
||||
assert keys == [
|
||||
"region",
|
||||
"bedrock_api_key",
|
||||
"aws_profile",
|
||||
"aws_access_key_id",
|
||||
"aws_secret_access_key",
|
||||
@@ -341,7 +377,7 @@ def test_bedrock_descriptor_and_builder():
|
||||
]
|
||||
assert [f.key for f in d.fields if f.required] == ["region"]
|
||||
secret = {f.key for f in d.fields if f.secret}
|
||||
assert secret == {"aws_secret_access_key", "aws_session_token"}
|
||||
assert secret == {"bedrock_api_key", "aws_secret_access_key", "aws_session_token"}
|
||||
# Recommended model is curated in the matrix (set_provider's auto-add depends on it).
|
||||
from coworker.providers.matrix import models_for_provider
|
||||
|
||||
@@ -416,6 +452,22 @@ def test_verify_bedrock_ok(monkeypatch):
|
||||
assert captured["client"]["region_name"] == "us-east-1"
|
||||
|
||||
|
||||
def test_verify_bedrock_api_key_rides_the_bearer_env(monkeypatch):
|
||||
import os
|
||||
|
||||
from coworker.providers.registry import verify_provider_key
|
||||
|
||||
monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False)
|
||||
captured: dict = {}
|
||||
_patch_session(monkeypatch, _FakeBedrockControl(), captured)
|
||||
out = verify_provider_key(
|
||||
"bedrock", fields={"region": "us-east-1", "bedrock_api_key": "ABSKtest"}
|
||||
)
|
||||
assert out == {"ok": True}
|
||||
assert captured["session"] == {} # bearer only — no SigV4 session kwargs
|
||||
assert os.environ["AWS_BEARER_TOKEN_BEDROCK"] == "ABSKtest"
|
||||
|
||||
|
||||
def test_verify_bedrock_maps_client_errors(monkeypatch):
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
|
||||
Reference in New Issue
Block a user