ocai-bedrock 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ocai_bedrock-0.1.1/.gitignore +60 -0
- ocai_bedrock-0.1.1/CHANGELOG.md +14 -0
- ocai_bedrock-0.1.1/PKG-INFO +80 -0
- ocai_bedrock-0.1.1/README.md +62 -0
- ocai_bedrock-0.1.1/pyproject.toml +32 -0
- ocai_bedrock-0.1.1/src/ocai/bedrock/__init__.py +5 -0
- ocai_bedrock-0.1.1/src/ocai/bedrock/hosting.py +163 -0
- ocai_bedrock-0.1.1/src/ocai/bedrock/outcomes.py +199 -0
- ocai_bedrock-0.1.1/src/ocai/bedrock/py.typed +0 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_blocked.json +117 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_denied.json +38 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_future.json +78 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_masked.json +117 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_model_error.json +40 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_text.json +88 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_throttled.json +38 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_tools.json +85 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_uncertain.json +70 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_unknown_error.json +38 -0
- ocai_bedrock-0.1.1/tests/http/bedrock_invoke_validation.json +38 -0
- ocai_bedrock-0.1.1/tests/test_host.py +500 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# uv / venv
|
|
11
|
+
.venv/
|
|
12
|
+
uv.lock.bak
|
|
13
|
+
|
|
14
|
+
# Tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.import_linter_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
|
|
22
|
+
# Docs site build output and page cache (zensical build)
|
|
23
|
+
/site/
|
|
24
|
+
/.cache/
|
|
25
|
+
|
|
26
|
+
# Node (future typescript/)
|
|
27
|
+
node_modules/
|
|
28
|
+
*.tsbuildinfo
|
|
29
|
+
|
|
30
|
+
# Go (future go/)
|
|
31
|
+
/go/bin/
|
|
32
|
+
|
|
33
|
+
# Editors / OS. Shared editor settings ARE committed: the multi-root workspace
|
|
34
|
+
# (ocai.code-workspace) and each folder's .vscode/settings.json + extensions.json
|
|
35
|
+
# give Pylance/Ruff the right interpreter per project. Personal files stay local.
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/*
|
|
38
|
+
!.vscode/settings.json
|
|
39
|
+
!.vscode/extensions.json
|
|
40
|
+
.DS_Store
|
|
41
|
+
|
|
42
|
+
# Per-user agent settings (never commit personal permissions)
|
|
43
|
+
.claude/settings.local.json
|
|
44
|
+
.codex/config.toml
|
|
45
|
+
|
|
46
|
+
# Platform build and browser-test artifacts
|
|
47
|
+
.nuxt/
|
|
48
|
+
.output/
|
|
49
|
+
platform/test-results/
|
|
50
|
+
platform/playwright-report/
|
|
51
|
+
|
|
52
|
+
# Local environment files: the root .env is the active one, .envs/ holds the
|
|
53
|
+
# per-environment variants (`task env -- <ENV>` copies .envs/.env.<ENV> to .env).
|
|
54
|
+
# Never committed: they hold credentials.
|
|
55
|
+
/.env
|
|
56
|
+
/.envs/
|
|
57
|
+
.local/
|
|
58
|
+
|
|
59
|
+
# Task execution fingerprints
|
|
60
|
+
.task/
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to ocai-bedrock are recorded here by the release workflow.
|
|
4
|
+
## 0.1.1 (2026-11-11)
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
#### Add an explicit Bedrock InvokeModel host for Anthropic unary calls, native errors,
|
|
9
|
+
|
|
10
|
+
and governed host metadata/guardrail effects through public ocai contracts. Keep
|
|
11
|
+
AWS authentication in ocai-aws and semantic conversion in ocai-anthropic (WU-48).
|
|
12
|
+
|
|
13
|
+
Strengthen package boundary checks for the independent cloud service adapter;
|
|
14
|
+
no kernel runtime or canonical contract changes.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ocai-bedrock
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Bedrock hosting through public ocai contracts.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ocarinow/ocai
|
|
6
|
+
Project-URL: Source, https://github.com/ocarinow/ocai
|
|
7
|
+
Project-URL: Documentation, https://ocarinow.github.io/ocai/
|
|
8
|
+
Project-URL: Changelog, https://github.com/ocarinow/ocai/blob/main/python/packages/ocai-bedrock/CHANGELOG.md
|
|
9
|
+
Author: ocai maintainers
|
|
10
|
+
License-Expression: FSL-1.1-ALv2
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Requires-Dist: ocai<0.2,>=0.1
|
|
13
|
+
Provides-Extra: anthropic
|
|
14
|
+
Requires-Dist: ocai-anthropic<0.2,>=0.1; extra == 'anthropic'
|
|
15
|
+
Provides-Extra: aws
|
|
16
|
+
Requires-Dist: ocai-aws<0.2,>=0.1; extra == 'aws'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# ocai-bedrock
|
|
20
|
+
|
|
21
|
+
Bedrock hosting for ocai, separate from shared AWS authentication and semantic
|
|
22
|
+
provider codecs. This package owns InvokeModel addressing, native host errors and
|
|
23
|
+
response metadata/effects. It performs no discovery and imports no provider SDK.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import ocai.chat
|
|
27
|
+
from ocai.aws import SigV4Credential
|
|
28
|
+
from ocai.bedrock import Bedrock
|
|
29
|
+
|
|
30
|
+
model = ocai.chat.model(
|
|
31
|
+
"anthropic/us.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
32
|
+
host=Bedrock(region="us-east-1"),
|
|
33
|
+
credentials=SigV4Credential(
|
|
34
|
+
access_key="synthetic-access-key", secret_key="synthetic-secret-key",
|
|
35
|
+
region="us-east-1",
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Install the Anthropic codec and AWS credential packages separately or use the
|
|
41
|
+
`anthropic` and `aws` extras. The application supplies credentials explicitly;
|
|
42
|
+
ocai-aws delegates signing to AWS-maintained code. Users do not import its SDK.
|
|
43
|
+
|
|
44
|
+
Qualified unary surface: text, function tools, trait-declared version/beta body
|
|
45
|
+
placement, encoded identifiers, native errors and response metadata/guardrail
|
|
46
|
+
accounting. Explicit profile aliases affect capability lookup only. No model-ID
|
|
47
|
+
heuristic is used. Existing preflight profiles include dialect-wide wildcard
|
|
48
|
+
declarations; those and coverage_complete are not hosted-model qualification
|
|
49
|
+
(G-15). Verify deployment support independently of dialect declarations.
|
|
50
|
+
|
|
51
|
+
`beta` is an explicit tuple of body beta values; it does not silently enable any
|
|
52
|
+
feature. Version/beta conflicts fail before I/O. Binding-level arbitrary headers
|
|
53
|
+
are transmitted as explicit caller data, not qualified as feature relocations.
|
|
54
|
+
Use the host's beta field or trait-declared feature mappings for that purpose.
|
|
55
|
+
|
|
56
|
+
Configure guardrail identifier/version together. Recognized trace assessments
|
|
57
|
+
separate blocked output (ContentFilterError) from masking (reported semantic
|
|
58
|
+
shift). An intervention without sufficient trace is reported as unclassified;
|
|
59
|
+
it is never guessed to be blocked or unmodified. FidelityPolicy can reject those
|
|
60
|
+
shifts. HostEvidence retains native headers/body and host metadata; traces can
|
|
61
|
+
contain original content, so application exporters must redact them. Passive
|
|
62
|
+
observers omit this evidence.
|
|
63
|
+
|
|
64
|
+
Streaming, Converse, media, managed prompts, regional discovery and compatibility
|
|
65
|
+
endpoints are not qualified here. Streaming fails before I/O. Recorded tests prove
|
|
66
|
+
wire behavior without live keys; they do not establish service/account access.
|
|
67
|
+
|
|
68
|
+
| Surface | Recorded qualification |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Text and system instruction | Exact body, canonical text/finish and input/output usage, both drivers |
|
|
71
|
+
| Function tools | Custom tool schema, auto selection, returned call identity/arguments and finish |
|
|
72
|
+
| Routing and beta | Encoded identifier, explicit version and beta placement; no beta feature availability claim |
|
|
73
|
+
| Errors | Header-only throttle/access/model error and body native codes; unknown codes preserved |
|
|
74
|
+
| Guardrails | Explicit blocking, output masking, uncertain intervention, caller fidelity rejection |
|
|
75
|
+
| Metadata | Host body fields and latency/tier headers retained; future host/dialect fields separated |
|
|
76
|
+
| Excluded | Streaming, Converse, media, managed prompts, regional/model discovery, live availability |
|
|
77
|
+
|
|
78
|
+
The recordings are synthetic, hand-authored against the AWS research sources in
|
|
79
|
+
`docs/providers/amazon-bedrock.md` section 16. They reuse the existing Anthropic
|
|
80
|
+
conversion graph. There is no new semantic edge or canonical revision.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ocai-bedrock
|
|
2
|
+
|
|
3
|
+
Bedrock hosting for ocai, separate from shared AWS authentication and semantic
|
|
4
|
+
provider codecs. This package owns InvokeModel addressing, native host errors and
|
|
5
|
+
response metadata/effects. It performs no discovery and imports no provider SDK.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import ocai.chat
|
|
9
|
+
from ocai.aws import SigV4Credential
|
|
10
|
+
from ocai.bedrock import Bedrock
|
|
11
|
+
|
|
12
|
+
model = ocai.chat.model(
|
|
13
|
+
"anthropic/us.anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
14
|
+
host=Bedrock(region="us-east-1"),
|
|
15
|
+
credentials=SigV4Credential(
|
|
16
|
+
access_key="synthetic-access-key", secret_key="synthetic-secret-key",
|
|
17
|
+
region="us-east-1",
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Install the Anthropic codec and AWS credential packages separately or use the
|
|
23
|
+
`anthropic` and `aws` extras. The application supplies credentials explicitly;
|
|
24
|
+
ocai-aws delegates signing to AWS-maintained code. Users do not import its SDK.
|
|
25
|
+
|
|
26
|
+
Qualified unary surface: text, function tools, trait-declared version/beta body
|
|
27
|
+
placement, encoded identifiers, native errors and response metadata/guardrail
|
|
28
|
+
accounting. Explicit profile aliases affect capability lookup only. No model-ID
|
|
29
|
+
heuristic is used. Existing preflight profiles include dialect-wide wildcard
|
|
30
|
+
declarations; those and coverage_complete are not hosted-model qualification
|
|
31
|
+
(G-15). Verify deployment support independently of dialect declarations.
|
|
32
|
+
|
|
33
|
+
`beta` is an explicit tuple of body beta values; it does not silently enable any
|
|
34
|
+
feature. Version/beta conflicts fail before I/O. Binding-level arbitrary headers
|
|
35
|
+
are transmitted as explicit caller data, not qualified as feature relocations.
|
|
36
|
+
Use the host's beta field or trait-declared feature mappings for that purpose.
|
|
37
|
+
|
|
38
|
+
Configure guardrail identifier/version together. Recognized trace assessments
|
|
39
|
+
separate blocked output (ContentFilterError) from masking (reported semantic
|
|
40
|
+
shift). An intervention without sufficient trace is reported as unclassified;
|
|
41
|
+
it is never guessed to be blocked or unmodified. FidelityPolicy can reject those
|
|
42
|
+
shifts. HostEvidence retains native headers/body and host metadata; traces can
|
|
43
|
+
contain original content, so application exporters must redact them. Passive
|
|
44
|
+
observers omit this evidence.
|
|
45
|
+
|
|
46
|
+
Streaming, Converse, media, managed prompts, regional discovery and compatibility
|
|
47
|
+
endpoints are not qualified here. Streaming fails before I/O. Recorded tests prove
|
|
48
|
+
wire behavior without live keys; they do not establish service/account access.
|
|
49
|
+
|
|
50
|
+
| Surface | Recorded qualification |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Text and system instruction | Exact body, canonical text/finish and input/output usage, both drivers |
|
|
53
|
+
| Function tools | Custom tool schema, auto selection, returned call identity/arguments and finish |
|
|
54
|
+
| Routing and beta | Encoded identifier, explicit version and beta placement; no beta feature availability claim |
|
|
55
|
+
| Errors | Header-only throttle/access/model error and body native codes; unknown codes preserved |
|
|
56
|
+
| Guardrails | Explicit blocking, output masking, uncertain intervention, caller fidelity rejection |
|
|
57
|
+
| Metadata | Host body fields and latency/tier headers retained; future host/dialect fields separated |
|
|
58
|
+
| Excluded | Streaming, Converse, media, managed prompts, regional/model discovery, live availability |
|
|
59
|
+
|
|
60
|
+
The recordings are synthetic, hand-authored against the AWS research sources in
|
|
61
|
+
`docs/providers/amazon-bedrock.md` section 16. They reuse the existing Anthropic
|
|
62
|
+
conversion graph. There is no new semantic edge or canonical revision.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ocai-bedrock"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Bedrock hosting through public ocai contracts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
license = "FSL-1.1-ALv2"
|
|
12
|
+
authors = [{ name = "ocai maintainers" }]
|
|
13
|
+
dependencies = ["ocai>=0.1,<0.2"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
aws = ["ocai-aws>=0.1,<0.2"]
|
|
17
|
+
anthropic = ["ocai-anthropic>=0.1,<0.2"]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/ocarinow/ocai"
|
|
21
|
+
Source = "https://github.com/ocarinow/ocai"
|
|
22
|
+
Documentation = "https://ocarinow.github.io/ocai/"
|
|
23
|
+
Changelog = "https://github.com/ocarinow/ocai/blob/main/python/packages/ocai-bedrock/CHANGELOG.md"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/ocai"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build]
|
|
29
|
+
dev-mode-dirs = ["src"]
|
|
30
|
+
|
|
31
|
+
[tool.ocai]
|
|
32
|
+
role = "host"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""External Bedrock hosting; semantic codecs and AWS authentication stay separate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from dataclasses import field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ocai.bedrock.outcomes import error_mapping
|
|
13
|
+
from ocai.bedrock.outcomes import response_mapping
|
|
14
|
+
from ocai.core import UnsupportedFeatureError
|
|
15
|
+
from ocai.wire.hosting import Custom
|
|
16
|
+
from ocai.wire.hosting import EndpointSpec
|
|
17
|
+
from ocai.wire.hosting import EnvelopeTransform
|
|
18
|
+
from ocai.wire.outcomes import HostErrorMapper
|
|
19
|
+
from ocai.wire.outcomes import HostResponseAdapter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class _InvokeEnvelope:
|
|
24
|
+
spec: EndpointSpec
|
|
25
|
+
beta: tuple[str, ...]
|
|
26
|
+
guardrail_config: Mapping[str, Any] | None
|
|
27
|
+
|
|
28
|
+
def wrap(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
29
|
+
result = deepcopy(payload)
|
|
30
|
+
if result.get("stream"):
|
|
31
|
+
raise UnsupportedFeatureError("Bedrock streaming is not qualified yet")
|
|
32
|
+
if "model" in result and result["model"] != self.spec.resource:
|
|
33
|
+
raise ValueError("payload model conflicts with the endpoint resource")
|
|
34
|
+
result.pop("model", None)
|
|
35
|
+
result.pop("stream", None)
|
|
36
|
+
additions: dict[str, Any] = dict(self.spec.traits.body_fields("bedrock"))
|
|
37
|
+
if self.beta:
|
|
38
|
+
if "anthropic_beta" in additions and additions["anthropic_beta"] != list(self.beta):
|
|
39
|
+
raise ValueError("conflicting Bedrock beta declarations")
|
|
40
|
+
additions["anthropic_beta"] = list(self.beta)
|
|
41
|
+
if self.guardrail_config is not None:
|
|
42
|
+
additions["amazon-bedrock-guardrailConfig"] = deepcopy(dict(self.guardrail_config))
|
|
43
|
+
for key, value in additions.items():
|
|
44
|
+
if key in result and result[key] != value:
|
|
45
|
+
raise ValueError(f"conflicting Bedrock body declaration: {key}")
|
|
46
|
+
result[key] = value
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
def unwrap(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
50
|
+
return payload
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Bedrock:
|
|
55
|
+
"""Host the qualified Anthropic Messages unary surface on InvokeModel.
|
|
56
|
+
|
|
57
|
+
Region and credentials are explicit; this value performs no discovery or I/O.
|
|
58
|
+
Pass it to ocai.chat.model with a separately supplied ocai credential. Optional
|
|
59
|
+
endpoint_url is explicit deployment configuration, not a discovery mechanism.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
region: str
|
|
63
|
+
endpoint_url: str | None = None
|
|
64
|
+
beta: tuple[str, ...] = ()
|
|
65
|
+
guardrail_identifier: str | None = None
|
|
66
|
+
guardrail_version: str | None = None
|
|
67
|
+
guardrail_config: Mapping[str, Any] | None = None
|
|
68
|
+
trace: str | None = None
|
|
69
|
+
performance_latency: str | None = None
|
|
70
|
+
service_tier: str | None = None
|
|
71
|
+
static_headers: Mapping[str, str] = field(default_factory=dict[str, str])
|
|
72
|
+
profile_aliases: Mapping[str, str] = field(default_factory=dict[str, str])
|
|
73
|
+
|
|
74
|
+
def __post_init__(self) -> None:
|
|
75
|
+
"""Validate explicit configuration and detach caller-owned maps."""
|
|
76
|
+
if not re.fullmatch(r"[a-z]{2}(?:-[a-z0-9]+)+-[0-9]+", self.region):
|
|
77
|
+
raise ValueError("an explicit AWS region name is required")
|
|
78
|
+
if (self.guardrail_identifier is None) != (self.guardrail_version is None):
|
|
79
|
+
raise ValueError("guardrail identifier and version must be supplied together")
|
|
80
|
+
if self.guardrail_config is not None and self.guardrail_identifier is None:
|
|
81
|
+
raise ValueError("guardrail body configuration requires identifier and version")
|
|
82
|
+
if any(
|
|
83
|
+
key.lower() in {"anthropic-version", "anthropic-beta"} for key in self.static_headers
|
|
84
|
+
):
|
|
85
|
+
raise ValueError("use declared body versions and beta, not static Anthropic headers")
|
|
86
|
+
object.__setattr__(self, "beta", tuple(self.beta))
|
|
87
|
+
object.__setattr__(self, "static_headers", dict(self.static_headers))
|
|
88
|
+
object.__setattr__(self, "profile_aliases", dict(self.profile_aliases))
|
|
89
|
+
object.__setattr__(self, "guardrail_config", deepcopy(self.guardrail_config))
|
|
90
|
+
self._target()
|
|
91
|
+
|
|
92
|
+
def _target(self) -> Custom:
|
|
93
|
+
suffix = "amazonaws.com.cn" if self.region.startswith("cn-") else "amazonaws.com"
|
|
94
|
+
origin = self.endpoint_url or f"https://bedrock-runtime.{self.region}.{suffix}"
|
|
95
|
+
headers = dict(self.static_headers)
|
|
96
|
+
headers.setdefault("accept", "application/json")
|
|
97
|
+
for name, value in (
|
|
98
|
+
("x-amzn-bedrock-guardrailidentifier", self.guardrail_identifier),
|
|
99
|
+
("x-amzn-bedrock-guardrailversion", self.guardrail_version),
|
|
100
|
+
("x-amzn-bedrock-trace", self.trace),
|
|
101
|
+
("x-amzn-bedrock-performanceconfig-latency", self.performance_latency),
|
|
102
|
+
("x-amzn-bedrock-service-tier", self.service_tier),
|
|
103
|
+
):
|
|
104
|
+
if value is not None:
|
|
105
|
+
if name in headers and headers[name] != value:
|
|
106
|
+
raise ValueError(f"conflicting Bedrock header declaration: {name}")
|
|
107
|
+
headers[name] = value
|
|
108
|
+
return Custom(
|
|
109
|
+
origin,
|
|
110
|
+
paths={"generative": "/model/{resource}/invoke"},
|
|
111
|
+
static_headers=headers,
|
|
112
|
+
profile_aliases=self.profile_aliases,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _qualify(spec: EndpointSpec) -> None:
|
|
117
|
+
if spec.op.streaming:
|
|
118
|
+
raise UnsupportedFeatureError("Bedrock streaming is not qualified yet")
|
|
119
|
+
if (spec.dialect.provider, spec.dialect.interface, spec.op.kind) != (
|
|
120
|
+
"anthropic",
|
|
121
|
+
"messages",
|
|
122
|
+
"generative",
|
|
123
|
+
):
|
|
124
|
+
raise UnsupportedFeatureError("only Anthropic Messages InvokeModel is qualified")
|
|
125
|
+
if spec.traits.host_body_versions.get("bedrock") is None:
|
|
126
|
+
raise UnsupportedFeatureError("dialect has no explicit Bedrock version declaration")
|
|
127
|
+
|
|
128
|
+
def endpoint(self, spec: EndpointSpec) -> str:
|
|
129
|
+
"""Place the unchanged resource in one encoded InvokeModel path segment."""
|
|
130
|
+
self._qualify(spec)
|
|
131
|
+
return self._target().endpoint(spec)
|
|
132
|
+
|
|
133
|
+
def headers(self, spec: EndpointSpec) -> dict[str, str]:
|
|
134
|
+
"""Use trait-declared relocations and explicit non-secret host headers."""
|
|
135
|
+
self._qualify(spec)
|
|
136
|
+
result = self._target().headers(spec)
|
|
137
|
+
relocated = {key.lower() for key in spec.traits.version_headers}
|
|
138
|
+
relocated.update(
|
|
139
|
+
rule.header for rule in spec.traits.host_feature_body_mappings.get("bedrock", ())
|
|
140
|
+
)
|
|
141
|
+
return {key: value for key, value in result.items() if key.lower() not in relocated}
|
|
142
|
+
|
|
143
|
+
def envelope(self, spec: EndpointSpec) -> EnvelopeTransform:
|
|
144
|
+
"""Relocate provider-bound resource/version fields without copying codecs."""
|
|
145
|
+
self._qualify(spec)
|
|
146
|
+
config = self.guardrail_config
|
|
147
|
+
if self.guardrail_identifier is not None and config is None:
|
|
148
|
+
config = dict[str, Any]()
|
|
149
|
+
return _InvokeEnvelope(spec, self.beta, config)
|
|
150
|
+
|
|
151
|
+
def profile_key(self, resource: str) -> str:
|
|
152
|
+
"""Use explicit aliases only; a cloud model identifier is not qualification."""
|
|
153
|
+
return self._target().profile_key(resource)
|
|
154
|
+
|
|
155
|
+
def error_mapper(self, spec: EndpointSpec) -> HostErrorMapper:
|
|
156
|
+
"""Select public native error adaptation for this qualified operation."""
|
|
157
|
+
self._qualify(spec)
|
|
158
|
+
return error_mapping()
|
|
159
|
+
|
|
160
|
+
def response_adapter(self, spec: EndpointSpec) -> HostResponseAdapter:
|
|
161
|
+
"""Select declared unary metadata and guardrail effects."""
|
|
162
|
+
self._qualify(spec)
|
|
163
|
+
return response_mapping()
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Bedrock-native outcome mapping behind provider-neutral host contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ocai.convert import DeclaredDisposition
|
|
8
|
+
from ocai.convert.host_effects import HostEffectContract
|
|
9
|
+
from ocai.core import AuthError
|
|
10
|
+
from ocai.core import ContentFilterError
|
|
11
|
+
from ocai.core import Disposition
|
|
12
|
+
from ocai.core import FieldLoss
|
|
13
|
+
from ocai.core import OcaiError
|
|
14
|
+
from ocai.core import RateLimitError
|
|
15
|
+
from ocai.core import SemanticShift
|
|
16
|
+
from ocai.core import TransientError
|
|
17
|
+
from ocai.core.ir import is_json_array
|
|
18
|
+
from ocai.core.ir import is_json_object
|
|
19
|
+
from ocai.wire.http import HttpResponse
|
|
20
|
+
from ocai.wire.outcomes import HostAdaptation
|
|
21
|
+
from ocai.wire.outcomes import HostErrorMapper
|
|
22
|
+
from ocai.wire.outcomes import HostResponseAdapter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _native_error(error: OcaiError, options: dict[str, Any]) -> OcaiError | None:
|
|
26
|
+
headers = {key.lower(): value for key, value in (error.response_headers or {}).items()}
|
|
27
|
+
raw = error.raw or {}
|
|
28
|
+
native = headers.get("x-amzn-errortype") or raw.get("__type") or raw.get("code")
|
|
29
|
+
if not isinstance(native, str) or not native:
|
|
30
|
+
return None
|
|
31
|
+
name = native.split(":", 1)[0].rsplit("#", 1)[-1]
|
|
32
|
+
error_type: type[OcaiError] | None = None
|
|
33
|
+
if name in {"ThrottlingException", "ModelNotReadyException", "ServiceQuotaExceededException"}:
|
|
34
|
+
error_type = RateLimitError
|
|
35
|
+
elif name in {
|
|
36
|
+
"ServiceUnavailableException",
|
|
37
|
+
"InternalServerException",
|
|
38
|
+
"InternalFailure",
|
|
39
|
+
"ModelTimeoutException",
|
|
40
|
+
"ModelStreamErrorException",
|
|
41
|
+
"ModelErrorException",
|
|
42
|
+
}:
|
|
43
|
+
error_type = TransientError
|
|
44
|
+
elif name in {
|
|
45
|
+
"AccessDeniedException",
|
|
46
|
+
"ExpiredTokenException",
|
|
47
|
+
"UnrecognizedClientException",
|
|
48
|
+
"NotAuthorized",
|
|
49
|
+
"IncompleteSignature",
|
|
50
|
+
"InvalidSignatureException",
|
|
51
|
+
}:
|
|
52
|
+
error_type = AuthError
|
|
53
|
+
if error_type is None:
|
|
54
|
+
error.code = native
|
|
55
|
+
return error
|
|
56
|
+
return error_type("Bedrock request failed", code=native)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def error_mapping() -> HostErrorMapper:
|
|
60
|
+
"""Build a detached native-code classifier; no status-only provider guessing."""
|
|
61
|
+
return HostErrorMapper("bedrock.invoke-errors@1", _native_error)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _assessment_actions(assessment: object) -> set[str]:
|
|
65
|
+
actions: set[str] = set()
|
|
66
|
+
if not is_json_object(assessment):
|
|
67
|
+
return actions
|
|
68
|
+
lanes = {
|
|
69
|
+
"topicPolicy": ("topics",),
|
|
70
|
+
"contentPolicy": ("filters",),
|
|
71
|
+
"wordPolicy": ("customWords", "managedWordLists"),
|
|
72
|
+
"sensitiveInformationPolicy": ("piiEntities", "regexes"),
|
|
73
|
+
}
|
|
74
|
+
for policy, names in lanes.items():
|
|
75
|
+
record = assessment.get(policy)
|
|
76
|
+
if not is_json_object(record):
|
|
77
|
+
continue
|
|
78
|
+
for name in names:
|
|
79
|
+
values = record.get(name)
|
|
80
|
+
if is_json_array(values):
|
|
81
|
+
actions.update(
|
|
82
|
+
value["action"]
|
|
83
|
+
for value in values
|
|
84
|
+
if is_json_object(value) and isinstance(value.get("action"), str)
|
|
85
|
+
)
|
|
86
|
+
return actions
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _actions(trace: object) -> tuple[set[str], set[str]]:
|
|
90
|
+
"""Read documented assessment lanes only; modelOutput is never classified."""
|
|
91
|
+
if not is_json_object(trace) or not is_json_object(guardrail := trace.get("guardrail")):
|
|
92
|
+
return set(), set()
|
|
93
|
+
input_actions: set[str] = set()
|
|
94
|
+
input_root = guardrail.get("input")
|
|
95
|
+
if is_json_object(input_root):
|
|
96
|
+
for assessment in input_root.values():
|
|
97
|
+
input_actions.update(_assessment_actions(assessment))
|
|
98
|
+
output_actions: set[str] = set()
|
|
99
|
+
outputs = guardrail.get("outputs")
|
|
100
|
+
for root in outputs if is_json_array(outputs) else []:
|
|
101
|
+
if is_json_object(root):
|
|
102
|
+
for assessment in root.values():
|
|
103
|
+
output_actions.update(_assessment_actions(assessment))
|
|
104
|
+
return input_actions, output_actions
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _response(
|
|
108
|
+
response: HttpResponse, payload: dict[str, Any], options: dict[str, Any]
|
|
109
|
+
) -> HostAdaptation:
|
|
110
|
+
metadata: dict[str, Any] = {"body": {}, "headers": {}}
|
|
111
|
+
losses: list[FieldLoss] = []
|
|
112
|
+
for key in tuple(payload):
|
|
113
|
+
if key.startswith("amazon-bedrock-"):
|
|
114
|
+
metadata["body"][key] = payload.pop(key)
|
|
115
|
+
losses.append(
|
|
116
|
+
FieldLoss(
|
|
117
|
+
f"body[{key}]", Disposition.RENAMED, note="Preserved in host evidence metadata"
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
for key, value in response.headers.items():
|
|
121
|
+
if key.lower() in {
|
|
122
|
+
"x-amzn-bedrock-service-tier",
|
|
123
|
+
"x-amzn-bedrock-performanceconfig-latency",
|
|
124
|
+
}:
|
|
125
|
+
metadata["headers"][key.lower()] = value
|
|
126
|
+
losses.append(
|
|
127
|
+
FieldLoss(
|
|
128
|
+
f"headers[{key.lower()}]",
|
|
129
|
+
Disposition.RENAMED,
|
|
130
|
+
note="Preserved in host evidence metadata",
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
action = metadata["body"].get("amazon-bedrock-guardrailAction")
|
|
134
|
+
trace = metadata["body"].get("amazon-bedrock-trace")
|
|
135
|
+
shifts: list[SemanticShift] = []
|
|
136
|
+
error: OcaiError | None = None
|
|
137
|
+
if action == "INTERVENED":
|
|
138
|
+
input_actions, output_actions = _actions(trace)
|
|
139
|
+
if "BLOCKED" in input_actions | output_actions:
|
|
140
|
+
shifts.append(
|
|
141
|
+
SemanticShift(
|
|
142
|
+
"bedrock.guardrail_blocked",
|
|
143
|
+
"response",
|
|
144
|
+
"An explicit guardrail assessment blocked the invocation",
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
error = ContentFilterError(
|
|
148
|
+
"Bedrock guardrail blocked the invocation", code="guardrail_intervened"
|
|
149
|
+
)
|
|
150
|
+
elif "ANONYMIZED" in output_actions:
|
|
151
|
+
shifts.append(
|
|
152
|
+
SemanticShift(
|
|
153
|
+
"bedrock.masked_output",
|
|
154
|
+
"response",
|
|
155
|
+
"An explicit guardrail assessment anonymized output",
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
shifts.append(
|
|
160
|
+
SemanticShift(
|
|
161
|
+
"bedrock.guardrail_intervened",
|
|
162
|
+
"response",
|
|
163
|
+
"Intervention is known; its exact effect is unqualified",
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
elif action is not None and action != "NONE":
|
|
167
|
+
shifts.append(
|
|
168
|
+
SemanticShift(
|
|
169
|
+
"bedrock.guardrail_intervened",
|
|
170
|
+
"response",
|
|
171
|
+
"Unknown guardrail action preserved without interpreting it",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
return HostAdaptation(payload, metadata, tuple(losses), tuple(shifts), error)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def response_mapping() -> HostResponseAdapter:
|
|
178
|
+
"""Declare unary host metadata and effects without changing Anthropic edges."""
|
|
179
|
+
contract = HostEffectContract(
|
|
180
|
+
census={
|
|
181
|
+
"body[]": DeclaredDisposition(
|
|
182
|
+
Disposition.RENAMED,
|
|
183
|
+
"Only amazon-bedrock-* body members move into HostEvidence.metadata",
|
|
184
|
+
),
|
|
185
|
+
"headers[]": DeclaredDisposition(
|
|
186
|
+
Disposition.RENAMED,
|
|
187
|
+
"Selected service-tier/performance headers copy into HostEvidence.metadata",
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
semantic_shifts=tuple(
|
|
191
|
+
SemanticShift(kind, "response")
|
|
192
|
+
for kind in (
|
|
193
|
+
"bedrock.guardrail_blocked",
|
|
194
|
+
"bedrock.masked_output",
|
|
195
|
+
"bedrock.guardrail_intervened",
|
|
196
|
+
)
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
return HostResponseAdapter("bedrock", "bedrock.invoke-response@1", _response, contract)
|
|
File without changes
|