From b489bcdde2a0fa83a30894e3f3a8e67ef920472c Mon Sep 17 00:00:00 2001 From: pk-zipstack Date: Tue, 5 May 2026 13:34:44 +0530 Subject: [PATCH 1/4] [FEAT] Allow Bedrock to fall through to boto3's default credential chain Match the S3/MinIO connector pattern: when AWS access keys are left blank on the Bedrock LLM and embedding adapter forms, drop them from the kwargs dict so boto3's default credential chain handles authentication. This unlocks IAM role / instance profile / IRSA / AWS Profile scenarios on hosts that already have ambient AWS credentials (e.g. EKS workers with IRSA, EC2 with an instance profile). - llm1/static/bedrock.json: clarify access-key descriptions to mention IRSA and instance profile (already non-required at v0.163.2 base). - embedding1/static/bedrock.json: drop aws_access_key_id and aws_secret_access_key from top-level required; same description fix; expose aws_profile_name for parity with the LLM form. - base1.py: AWSBedrockLLMParameters and AWSBedrockEmbeddingParameters now strip empty access-key values from the validated kwargs before returning, so empty strings don't override boto3's default chain. AWSBedrockEmbeddingParameters fields gain explicit None defaults and an aws_profile_name field. Backward-compatible: existing adapters with access keys filled in continue to work unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../sdk1/src/unstract/sdk1/adapters/base1.py | 25 ++++++++++++++++--- .../adapters/embedding1/static/bedrock.json | 11 +++++--- .../sdk1/adapters/llm1/static/bedrock.json | 4 +-- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py index f7d98f0fa1..5a3a89ac39 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py @@ -538,6 +538,13 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: if enable_thinking and "thinking" in result_metadata: validated["thinking"] = result_metadata["thinking"] + # Strip empty AWS access keys so boto3's default credential chain + # takes over (instance profile / IRSA / AWS Profile / env vars). + # Mirrors the pattern in the S3/MinIO connector. + for key in ("aws_access_key_id", "aws_secret_access_key"): + if not validated.get(key): + validated.pop(key, None) + return validated @staticmethod @@ -959,9 +966,10 @@ def validate_model(adapter_metadata: dict[str, "Any"]) -> str: class AWSBedrockEmbeddingParameters(BaseEmbeddingParameters): """See https://docs.litellm.ai/docs/providers/bedrock.""" - aws_access_key_id: str | None - aws_secret_access_key: str | None - aws_region_name: str | None + aws_access_key_id: str | None = None + aws_secret_access_key: str | None = None + aws_region_name: str | None = None + aws_profile_name: str | None = None # For AWS SSO authentication @staticmethod def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: @@ -973,7 +981,16 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: ): adapter_metadata["aws_region_name"] = adapter_metadata["region_name"] - return AWSBedrockEmbeddingParameters(**adapter_metadata).model_dump() + validated = AWSBedrockEmbeddingParameters(**adapter_metadata).model_dump() + + # Strip empty AWS access keys so boto3's default credential chain + # takes over (instance profile / IRSA / AWS Profile / env vars). + # Mirrors the pattern in the S3/MinIO connector. + for key in ("aws_access_key_id", "aws_secret_access_key"): + if not validated.get(key): + validated.pop(key, None) + + return validated @staticmethod def validate_model(adapter_metadata: dict[str, "Any"]) -> str: diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json index b71028148c..a24db8d876 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json @@ -2,9 +2,7 @@ "title": "Bedrock Embeddings", "type": "object", "required": [ - "aws_secret_access_key", "region_name", - "aws_access_key_id", "model", "adapter_name" ], @@ -24,13 +22,13 @@ "aws_access_key_id": { "type": "string", "title": "AWS Access Key ID", - "description": "Provide your AWS Access Key ID", + "description": "Leave blank to use the host's IAM role / instance profile / IRSA, or AWS Profile.", "format": "password" }, "aws_secret_access_key": { "type": "string", "title": "AWS Secret Access Key", - "description": "Provide your AWS Secret Access Key", + "description": "Leave blank to use the host's IAM role / instance profile / IRSA, or AWS Profile.", "format": "password" }, "region_name": { @@ -38,6 +36,11 @@ "title": "AWS Region name", "description": "Provide the AWS Region name where the service is running. Eg. us-east-1" }, + "aws_profile_name": { + "type": "string", + "title": "AWS Profile Name", + "description": "AWS SSO profile name for authentication. Use this instead of access keys when using AWS SSO. Example: dev-profile" + }, "max_retries": { "type": "number", "minimum": 0, diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json index 9adb8b8ceb..b71aec8a53 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json @@ -22,13 +22,13 @@ "aws_access_key_id": { "type": "string", "title": "AWS Access Key ID", - "description": "Provide your AWS Access Key ID. Leave empty if using AWS Profile or IAM role.", + "description": "Leave blank to use the host's IAM role / instance profile / IRSA, or AWS Profile.", "format": "password" }, "aws_secret_access_key": { "type": "string", "title": "AWS Secret Access Key", - "description": "Provide your AWS Secret Access Key. Leave empty if using AWS Profile or IAM role.", + "description": "Leave blank to use the host's IAM role / instance profile / IRSA, or AWS Profile.", "format": "password" }, "region_name": { From 7f909b5794899f3fca4bdb5424365f240e3c3094 Mon Sep 17 00:00:00 2001 From: pk-zipstack Date: Tue, 5 May 2026 13:40:38 +0530 Subject: [PATCH 2/4] [FEAT] Add Authentication Type selector to Bedrock adapter form Add an explicit `auth_type` selector with two options, making the auth choice clear to users: - "Access Keys" (default): existing flow, keys required - "IAM Role / Instance Profile (on-prem AWS only)": no fields; relies on boto3's default credential chain (IRSA on EKS, task role on ECS, instance profile on EC2). Description on the selector explicitly notes this option is only for AWS-hosted Unstract deployments. The form-only auth_type field is stripped before LiteLLM validation in both AWSBedrockLLMParameters.validate() and AWSBedrockEmbeddingParameters. validate(). Empty access keys continue to be stripped so boto3 falls through to the default chain even when the access_keys arm is selected without values (matches the S3/MinIO connector pattern). Backward-compatible: legacy adapters without auth_type behave as "Access Keys" mode (the default), and existing keys are forwarded unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../sdk1/src/unstract/sdk1/adapters/base1.py | 12 +++- .../adapters/embedding1/static/bedrock.json | 66 +++++++++++++++---- .../sdk1/adapters/llm1/static/bedrock.json | 66 +++++++++++++++---- 3 files changed, 117 insertions(+), 27 deletions(-) diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py index 5a3a89ac39..c5905d3d22 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py @@ -525,11 +525,12 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: result_metadata["thinking"] = thinking_config result_metadata["temperature"] = 1 - # Create validation metadata excluding control fields + # Create validation metadata excluding control fields. `auth_type` is + # a UI-only selector that drives form rendering; LiteLLM never sees it. validation_metadata = { k: v for k, v in result_metadata.items() - if k not in ("enable_thinking", "budget_tokens", "thinking") + if k not in ("enable_thinking", "budget_tokens", "thinking", "auth_type") } validated = AWSBedrockLLMParameters(**validation_metadata).model_dump() @@ -981,7 +982,12 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: ): adapter_metadata["aws_region_name"] = adapter_metadata["region_name"] - validated = AWSBedrockEmbeddingParameters(**adapter_metadata).model_dump() + # `auth_type` is a UI-only selector; strip before LiteLLM kwargs. + validation_metadata = { + k: v for k, v in adapter_metadata.items() if k != "auth_type" + } + + validated = AWSBedrockEmbeddingParameters(**validation_metadata).model_dump() # Strip empty AWS access keys so boto3's default credential chain # takes over (instance profile / IRSA / AWS Profile / env vars). diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json index a24db8d876..7f04e42425 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json @@ -19,18 +19,6 @@ "default": "amazon.titan-embed-text-v2:0", "description": "Model name. Refer to [Bedrock's documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html) for the list of available models." }, - "aws_access_key_id": { - "type": "string", - "title": "AWS Access Key ID", - "description": "Leave blank to use the host's IAM role / instance profile / IRSA, or AWS Profile.", - "format": "password" - }, - "aws_secret_access_key": { - "type": "string", - "title": "AWS Secret Access Key", - "description": "Leave blank to use the host's IAM role / instance profile / IRSA, or AWS Profile.", - "format": "password" - }, "region_name": { "type": "string", "title": "AWS Region name", @@ -56,6 +44,60 @@ "title": "Timeout", "default": 900, "description": "Timeout in seconds" + }, + "auth_type": { + "type": "string", + "title": "Authentication Type", + "enum": [ + "access_keys", + "iam_role" + ], + "enumNames": [ + "Access Keys", + "IAM Role / Instance Profile (on-prem AWS only)" + ], + "default": "access_keys", + "description": "Choose **Access Keys** for any deployment (provide your AWS Access Key ID and Secret). Choose **IAM Role / Instance Profile** only when Unstract is hosted on AWS infrastructure that already has ambient credentials — for example EKS pods with IRSA, ECS tasks with a task role, or EC2 instances with an instance profile. The on-prem option requires no further input; boto3 picks up the host's identity automatically." + } + }, + "dependencies": { + "auth_type": { + "oneOf": [ + { + "properties": { + "auth_type": { + "enum": [ + "access_keys" + ] + }, + "aws_access_key_id": { + "type": "string", + "title": "AWS Access Key ID", + "description": "Provide your AWS Access Key ID.", + "format": "password" + }, + "aws_secret_access_key": { + "type": "string", + "title": "AWS Secret Access Key", + "description": "Provide your AWS Secret Access Key.", + "format": "password" + } + }, + "required": [ + "aws_access_key_id", + "aws_secret_access_key" + ] + }, + { + "properties": { + "auth_type": { + "enum": [ + "iam_role" + ] + } + } + } + ] } } } diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json index b71aec8a53..03f23d1376 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/llm1/static/bedrock.json @@ -19,18 +19,6 @@ "default": "amazon.titan-text-express-v1", "description": "Model name. Refer to [Bedrock's documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html) for the list of available models." }, - "aws_access_key_id": { - "type": "string", - "title": "AWS Access Key ID", - "description": "Leave blank to use the host's IAM role / instance profile / IRSA, or AWS Profile.", - "format": "password" - }, - "aws_secret_access_key": { - "type": "string", - "title": "AWS Secret Access Key", - "description": "Leave blank to use the host's IAM role / instance profile / IRSA, or AWS Profile.", - "format": "password" - }, "region_name": { "type": "string", "title": "AWS Region Name", @@ -75,6 +63,60 @@ "title": "Enable Extended Thinking", "default": false, "description": "Enhance reasoning for complex tasks with step-by-step transparency. Available only for Claude 3.7 Sonnet." + }, + "auth_type": { + "type": "string", + "title": "Authentication Type", + "enum": [ + "access_keys", + "iam_role" + ], + "enumNames": [ + "Access Keys", + "IAM Role / Instance Profile (on-prem AWS only)" + ], + "default": "access_keys", + "description": "Choose **Access Keys** for any deployment (provide your AWS Access Key ID and Secret). Choose **IAM Role / Instance Profile** only when Unstract is hosted on AWS infrastructure that already has ambient credentials — for example EKS pods with IRSA, ECS tasks with a task role, or EC2 instances with an instance profile. The on-prem option requires no further input; boto3 picks up the host's identity automatically." + } + }, + "dependencies": { + "auth_type": { + "oneOf": [ + { + "properties": { + "auth_type": { + "enum": [ + "access_keys" + ] + }, + "aws_access_key_id": { + "type": "string", + "title": "AWS Access Key ID", + "description": "Provide your AWS Access Key ID.", + "format": "password" + }, + "aws_secret_access_key": { + "type": "string", + "title": "AWS Secret Access Key", + "description": "Provide your AWS Secret Access Key.", + "format": "password" + } + }, + "required": [ + "aws_access_key_id", + "aws_secret_access_key" + ] + }, + { + "properties": { + "auth_type": { + "enum": [ + "iam_role" + ] + } + } + } + ] } }, "allOf": [ From 3daa66d881fe8afe4e862aea027b6b9fe45d9627 Mon Sep 17 00:00:00 2001 From: pk-zipstack Date: Wed, 6 May 2026 11:27:17 +0530 Subject: [PATCH 3/4] [REVIEW] Address Bedrock auth_type review feedback Fixes the P0/P1 issues raised by greptile-apps and jaseemjaskp on PR #1944. Behaviour fixes: - Stale-key leak in IAM Role mode: switching an existing adapter from Access Keys to IAM Role would carry truthy stored access keys through the strip-empty-only loop, so boto3 silently authenticated with the old long-lived credentials instead of falling through to the host's IRSA / instance-profile identity. Both LLM and embedding paths were affected. - Silent acceptance of unknown auth_type: a typo (e.g. "access_key") or a malformed payload from a non-UI client passed through the dict comprehension untouched, with no enum guard. - Cross-field validation gap: explicit Access Keys mode with blank or whitespace-only values silently fell through to the default credential chain instead of surfacing the misconfiguration. Implementation: - Add a module-level _resolve_bedrock_aws_credentials helper used by both AWSBedrockLLMParameters.validate() and AWSBedrock EmbeddingParameters.validate(), so the auth-type contract is expressed once. - Validates auth_type against an allowlist (None | "access_keys" | "iam_role"); raises ValueError on anything else. - iam_role: unconditionally drops aws_access_key_id and aws_secret_access_key. - access_keys (explicit): requires non-blank values; raises ValueError if either is empty or whitespace-only. - Legacy (auth_type absent): retains the lenient strip behaviour so pre-PR adapter configurations continue to deserialise unchanged. - Restore aws_region_name as required (no `= None` default) on AWSBedrockEmbeddingParameters; only credentials may legitimately be absent. - Drop the orphan aws_profile_name field from embedding1/static/bedrock.json: it was added for parity with the LLM form but lives outside the auth_type oneOf and contradicts the selector's "no further input" semantics. The LLM form already had aws_profile_name pre-PR and is left alone for backwards compatibility. Tests: - New tests/test_bedrock_adapter.py covers 15 cases across LLM and embedding adapters: legacy-no-auth-type, explicit access_keys with valid/blank/whitespace keys, iam_role with stale/no keys, unknown auth_type rejection, cross-field validation, and preservation of unrelated params (model_id, aws_profile_name, region, thinking). Skipped (P2 nice-to-have): - Comment-scope clarification, MinIO reference rewording, validate-mutates-caller'\''s-dict, and the LLM form description nit about aws_profile_name visibility. These don'\''t change behaviour and can be addressed in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../sdk1/src/unstract/sdk1/adapters/base1.py | 95 +++++-- .../adapters/embedding1/static/bedrock.json | 5 - unstract/sdk1/tests/test_bedrock_adapter.py | 247 ++++++++++++++++++ 3 files changed, 324 insertions(+), 23 deletions(-) create mode 100644 unstract/sdk1/tests/test_bedrock_adapter.py diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py index c5905d3d22..57a5680bdf 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py @@ -10,6 +10,7 @@ from typing import Any from pydantic import BaseModel, Field, model_validator + from unstract.sdk1.adapters.constants import Common from unstract.sdk1.adapters.enums import AdapterTypes @@ -474,6 +475,71 @@ def validate_model(adapter_metadata: dict[str, "Any"]) -> str: return f"vertex_ai/{model}" +# AWS Bedrock auth helpers: shared by LLM and Embedding param classes. +# `auth_type` is a UI-only selector (Access Keys vs IAM Role / Instance +# Profile) that drives form rendering. The backend translates the user's +# choice into actual credential handling here so that both validate() +# methods stay symmetric and a single bug fix applies to both paths. +_BEDROCK_AWS_KEY_FIELDS: tuple[str, ...] = ( + "aws_access_key_id", + "aws_secret_access_key", +) +_BEDROCK_VALID_AUTH_TYPES: frozenset[str | None] = frozenset( + {None, "access_keys", "iam_role"} +) + + +def _resolve_bedrock_aws_credentials( + adapter_metadata: dict[str, "Any"], + validated: dict[str, "Any"], +) -> dict[str, "Any"]: + """Apply auth_type semantics to the validated LiteLLM kwargs. + + Three cases: + - ``auth_type == "iam_role"``: drop access keys unconditionally so a + previously-saved adapter switched into IAM Role mode does not leak + stale long-lived credentials. boto3's default credential chain + (IRSA / instance profile / env vars / AWS Profile) takes over. + - ``auth_type == "access_keys"`` (explicit): require non-blank values. + A blank submission must surface as a clear error rather than + silently fall through to the default chain (which would hide the + mistake and authenticate with whatever ambient creds the host has). + - ``auth_type is None`` (legacy adapters created before this field + existed): lenient strip of empty/missing keys. Preserves backwards + compatibility for stored configurations. + + Raises: + ValueError: on unknown ``auth_type`` (typo / non-UI client) or on + blank credentials when ``auth_type == "access_keys"``. + """ + auth_type = adapter_metadata.get("auth_type") + if auth_type not in _BEDROCK_VALID_AUTH_TYPES: + raise ValueError( + f"Unknown auth_type {auth_type!r}; expected one of " + f"{sorted(t for t in _BEDROCK_VALID_AUTH_TYPES if t)!r} or absent." + ) + + if auth_type == "iam_role": + for key in _BEDROCK_AWS_KEY_FIELDS: + validated.pop(key, None) + return validated + + if auth_type == "access_keys": + for key in _BEDROCK_AWS_KEY_FIELDS: + value = validated.get(key) + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{key} is required when auth_type is 'access_keys'.") + return validated + + # Legacy adapters with no auth_type: strip blanks silently to + # preserve the pre-PR behaviour where empty key fields fell through + # to boto3's default chain. + for key in _BEDROCK_AWS_KEY_FIELDS: + if not validated.get(key): + validated.pop(key, None) + return validated + + class AWSBedrockLLMParameters(BaseChatCompletionParameters): """See https://docs.litellm.ai/docs/providers/bedrock.""" @@ -539,14 +605,11 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: if enable_thinking and "thinking" in result_metadata: validated["thinking"] = result_metadata["thinking"] - # Strip empty AWS access keys so boto3's default credential chain - # takes over (instance profile / IRSA / AWS Profile / env vars). - # Mirrors the pattern in the S3/MinIO connector. - for key in ("aws_access_key_id", "aws_secret_access_key"): - if not validated.get(key): - validated.pop(key, None) - - return validated + # Apply Bedrock auth semantics: IAM Role mode drops keys, Access + # Keys mode requires non-blank values, legacy (no auth_type) is + # lenient. Reads auth_type from result_metadata since validation_ + # metadata strips it before Pydantic. + return _resolve_bedrock_aws_credentials(result_metadata, validated) @staticmethod def validate_model(adapter_metadata: dict[str, "Any"]) -> str: @@ -967,10 +1030,11 @@ def validate_model(adapter_metadata: dict[str, "Any"]) -> str: class AWSBedrockEmbeddingParameters(BaseEmbeddingParameters): """See https://docs.litellm.ai/docs/providers/bedrock.""" + # Region is still mandatory — credentials are the only fields that + # may be absent (IAM Role / Instance Profile mode). aws_access_key_id: str | None = None aws_secret_access_key: str | None = None - aws_region_name: str | None = None - aws_profile_name: str | None = None # For AWS SSO authentication + aws_region_name: str | None @staticmethod def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: @@ -989,14 +1053,9 @@ def validate(adapter_metadata: dict[str, "Any"]) -> dict[str, "Any"]: validated = AWSBedrockEmbeddingParameters(**validation_metadata).model_dump() - # Strip empty AWS access keys so boto3's default credential chain - # takes over (instance profile / IRSA / AWS Profile / env vars). - # Mirrors the pattern in the S3/MinIO connector. - for key in ("aws_access_key_id", "aws_secret_access_key"): - if not validated.get(key): - validated.pop(key, None) - - return validated + # Apply Bedrock auth semantics: IAM Role drops keys, Access Keys + # requires non-blank values, legacy (no auth_type) is lenient. + return _resolve_bedrock_aws_credentials(adapter_metadata, validated) @staticmethod def validate_model(adapter_metadata: dict[str, "Any"]) -> str: diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json index 7f04e42425..070c5a9e10 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json +++ b/unstract/sdk1/src/unstract/sdk1/adapters/embedding1/static/bedrock.json @@ -24,11 +24,6 @@ "title": "AWS Region name", "description": "Provide the AWS Region name where the service is running. Eg. us-east-1" }, - "aws_profile_name": { - "type": "string", - "title": "AWS Profile Name", - "description": "AWS SSO profile name for authentication. Use this instead of access keys when using AWS SSO. Example: dev-profile" - }, "max_retries": { "type": "number", "minimum": 0, diff --git a/unstract/sdk1/tests/test_bedrock_adapter.py b/unstract/sdk1/tests/test_bedrock_adapter.py new file mode 100644 index 0000000000..905016fe92 --- /dev/null +++ b/unstract/sdk1/tests/test_bedrock_adapter.py @@ -0,0 +1,247 @@ +"""Unit tests for the AWS Bedrock LLM and embedding adapters. + +Covers the auth_type selector behaviour added alongside the IAM Role / +Instance Profile mode, plus backwards compatibility for legacy adapter +configurations stored before auth_type existed. +""" + +import pytest + +from unstract.sdk1.adapters.base1 import ( + AWSBedrockEmbeddingParameters, + AWSBedrockLLMParameters, +) + +# ── LLM: validate auth_type semantics ──────────────────────────────────────── + + +def test_llm_legacy_no_auth_type_keeps_keys() -> None: + """Legacy adapters without auth_type must keep working unchanged.""" + out = AWSBedrockLLMParameters.validate( + { + "model": "anthropic.claude-3-haiku-20240307-v1:0", + "region_name": "us-east-1", + "aws_access_key_id": "AKIAFAKE", + "aws_secret_access_key": "secret", + } + ) + assert out["aws_access_key_id"] == "AKIAFAKE" + assert out["aws_secret_access_key"] == "secret" + assert out["aws_region_name"] == "us-east-1" + assert "auth_type" not in out + + +def test_llm_access_keys_mode_keeps_keys_and_strips_auth_type() -> None: + out = AWSBedrockLLMParameters.validate( + { + "auth_type": "access_keys", + "model": "anthropic.claude-3-haiku-20240307-v1:0", + "region_name": "us-east-1", + "aws_access_key_id": "AKIAFAKE", + "aws_secret_access_key": "secret", + } + ) + assert out["aws_access_key_id"] == "AKIAFAKE" + assert out["aws_secret_access_key"] == "secret" + assert "auth_type" not in out + + +def test_llm_iam_role_mode_drops_keys_even_when_present() -> None: + """IAM Role mode unconditionally drops access keys. + + A saved adapter switched into IAM mode must not silently leak the + previously stored long-lived credentials. + """ + out = AWSBedrockLLMParameters.validate( + { + "auth_type": "iam_role", + "model": "us.anthropic.claude-haiku-4-5-20251001-v1:0", + "region_name": "us-east-1", + "aws_access_key_id": "STALE_KEY", + "aws_secret_access_key": "STALE_SECRET", + } + ) + assert "aws_access_key_id" not in out + assert "aws_secret_access_key" not in out + assert out["aws_region_name"] == "us-east-1" + assert "auth_type" not in out + + +def test_llm_iam_role_mode_with_no_keys() -> None: + out = AWSBedrockLLMParameters.validate( + { + "auth_type": "iam_role", + "model": "us.anthropic.claude-haiku-4-5-20251001-v1:0", + "region_name": "us-east-1", + } + ) + assert "aws_access_key_id" not in out + assert "aws_secret_access_key" not in out + + +def test_llm_access_keys_mode_blank_keys_raises() -> None: + """Blank values must surface a clear error. + + Falling through to boto3's default chain would hide the user's + misconfiguration and authenticate with whatever ambient creds the + host happens to have. + """ + with pytest.raises(ValueError, match="aws_access_key_id is required"): + AWSBedrockLLMParameters.validate( + { + "auth_type": "access_keys", + "model": "anthropic.claude-3-haiku-20240307-v1:0", + "region_name": "us-east-1", + "aws_access_key_id": "", + "aws_secret_access_key": "", + } + ) + + +def test_llm_access_keys_mode_whitespace_keys_raises() -> None: + with pytest.raises(ValueError, match="aws_secret_access_key is required"): + AWSBedrockLLMParameters.validate( + { + "auth_type": "access_keys", + "model": "anthropic.claude-3-haiku-20240307-v1:0", + "region_name": "us-east-1", + "aws_access_key_id": "AKIAFAKE", + "aws_secret_access_key": " ", + } + ) + + +def test_llm_unknown_auth_type_raises() -> None: + """A typo or non-UI client must not silently fall through.""" + with pytest.raises(ValueError, match="Unknown auth_type"): + AWSBedrockLLMParameters.validate( + { + "auth_type": "access_key", # typo: missing 's' + "model": "anthropic.claude-3-haiku-20240307-v1:0", + "region_name": "us-east-1", + "aws_access_key_id": "AKIAFAKE", + "aws_secret_access_key": "secret", + } + ) + + +def test_llm_other_params_preserved_through_strip() -> None: + """Non-credential params survive the auth-type handling. + + model_id, aws_profile_name, region, and thinking config must pass + through both the strip and the resolver unchanged. + """ + out = AWSBedrockLLMParameters.validate( + { + "auth_type": "iam_role", + "model": "anthropic.claude-3-7-sonnet-20250219-v1:0", + "region_name": "us-east-1", + "aws_profile_name": "dev-profile", + "model_id": ( + "arn:aws:bedrock:us-east-1:1234:application-inference-profile/abc" + ), + "enable_thinking": True, + "budget_tokens": 4096, + } + ) + assert out["aws_profile_name"] == "dev-profile" + assert out["aws_region_name"] == "us-east-1" + assert out["model_id"].endswith("application-inference-profile/abc") + assert out["thinking"] == {"type": "enabled", "budget_tokens": 4096} + + +# ── Embedding: same auth_type matrix ───────────────────────────────────────── + + +def test_embedding_legacy_no_auth_type_keeps_keys() -> None: + out = AWSBedrockEmbeddingParameters.validate( + { + "model": "amazon.titan-embed-text-v2:0", + "region_name": "us-east-1", + "aws_access_key_id": "AKIAFAKE", + "aws_secret_access_key": "secret", + } + ) + assert out["aws_access_key_id"] == "AKIAFAKE" + assert out["aws_secret_access_key"] == "secret" + assert "auth_type" not in out + + +def test_embedding_access_keys_mode_keeps_keys() -> None: + out = AWSBedrockEmbeddingParameters.validate( + { + "auth_type": "access_keys", + "model": "amazon.titan-embed-text-v2:0", + "region_name": "us-east-1", + "aws_access_key_id": "AKIAFAKE", + "aws_secret_access_key": "secret", + } + ) + assert out["aws_access_key_id"] == "AKIAFAKE" + assert out["aws_secret_access_key"] == "secret" + assert "auth_type" not in out + + +def test_embedding_iam_role_mode_drops_stale_keys() -> None: + """Embedding-side parity with the LLM stale-key fix.""" + out = AWSBedrockEmbeddingParameters.validate( + { + "auth_type": "iam_role", + "model": "amazon.titan-embed-text-v2:0", + "region_name": "us-east-1", + "aws_access_key_id": "STALE_KEY", + "aws_secret_access_key": "STALE_SECRET", + } + ) + assert "aws_access_key_id" not in out + assert "aws_secret_access_key" not in out + assert out["aws_region_name"] == "us-east-1" + + +def test_embedding_iam_role_mode_with_no_keys() -> None: + out = AWSBedrockEmbeddingParameters.validate( + { + "auth_type": "iam_role", + "model": "amazon.titan-embed-text-v2:0", + "region_name": "us-east-1", + } + ) + assert "aws_access_key_id" not in out + assert "aws_secret_access_key" not in out + + +def test_embedding_access_keys_mode_blank_keys_raises() -> None: + with pytest.raises(ValueError, match="aws_access_key_id is required"): + AWSBedrockEmbeddingParameters.validate( + { + "auth_type": "access_keys", + "model": "amazon.titan-embed-text-v2:0", + "region_name": "us-east-1", + "aws_access_key_id": "", + "aws_secret_access_key": "", + } + ) + + +def test_embedding_unknown_auth_type_raises() -> None: + with pytest.raises(ValueError, match="Unknown auth_type"): + AWSBedrockEmbeddingParameters.validate( + { + "auth_type": "iamrole", # typo + "model": "amazon.titan-embed-text-v2:0", + "region_name": "us-east-1", + } + ) + + +def test_embedding_region_required_when_absent() -> None: + """aws_region_name is still mandatory even though credentials are not.""" + from pydantic import ValidationError + + with pytest.raises(ValidationError): + AWSBedrockEmbeddingParameters.validate( + { + "auth_type": "iam_role", + "model": "amazon.titan-embed-text-v2:0", + } + ) From 7d080c53e22003f7a83428a26f1e0b1dec8e71c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 05:57:45 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- unstract/sdk1/src/unstract/sdk1/adapters/base1.py | 1 - unstract/sdk1/tests/test_bedrock_adapter.py | 1 - 2 files changed, 2 deletions(-) diff --git a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py index 57a5680bdf..bac149b9ff 100644 --- a/unstract/sdk1/src/unstract/sdk1/adapters/base1.py +++ b/unstract/sdk1/src/unstract/sdk1/adapters/base1.py @@ -10,7 +10,6 @@ from typing import Any from pydantic import BaseModel, Field, model_validator - from unstract.sdk1.adapters.constants import Common from unstract.sdk1.adapters.enums import AdapterTypes diff --git a/unstract/sdk1/tests/test_bedrock_adapter.py b/unstract/sdk1/tests/test_bedrock_adapter.py index 905016fe92..4d4b7c1cd7 100644 --- a/unstract/sdk1/tests/test_bedrock_adapter.py +++ b/unstract/sdk1/tests/test_bedrock_adapter.py @@ -6,7 +6,6 @@ """ import pytest - from unstract.sdk1.adapters.base1 import ( AWSBedrockEmbeddingParameters, AWSBedrockLLMParameters,