simbiot-harness/venv/lib/python3.12/site-packages/strands/models/_openai_bedrock.py
Simbiot Harness b60439152b Simbiot Harness v0.1: тактовая лестница на Strands + CQRS/ES-спина
- harness/es.py: append-only event store + проекции (образец — кооп-портал)
- harness/levels.py: уровни 1с/1м/1ч/1д, контекст ∝ масштабу
- harness/scheduler.py: уровневые потоки, идемпотентные тики, absorbed-стабы
- harness/isolated_runner.py + _llm_worker.py: LLM-тик в сабпроцессе
  (глобальный asyncio-рантайм Strands не переживает 2+ create_harness)
- cli.py: init/task/tasks/status/events + requeue (написан самим минутным агентом)
- 9router: отдельный ключ sk-simbiot, модель auto (glm-5.3 первым)
2026-09-29 15:28:50 +00:00

191 lines
8.9 KiB
Python

"""Internal helpers for routing OpenAI-compatible clients to Amazon Bedrock.
Converts a ``bedrock_mantle_config`` dict into the ``base_url`` and ``api_key`` that the
OpenAI Python SDK consumes. Tokens are minted on demand via
``aws_bedrock_token_generator.provide_token`` so long-running agents survive the
bearer token's maximum lifetime.
The config's ``endpoint`` key selects which of Bedrock's two OpenAI-compatible endpoint
families serves the request; see :class:`BedrockMantleConfig`.
"""
from __future__ import annotations
from datetime import timedelta
from typing import Any, Literal, TypedDict
import boto3
from botocore.credentials import CredentialProvider
from ._validation import validate_region
_MANTLE_BASE_URL_TEMPLATE = "https://bedrock-mantle.{region}.api.aws{path}"
# On bedrock-runtime every OpenAI-compatible API is served from /openai/v1, so the base
# path is fixed rather than per-model as it is on bedrock-mantle.
_RUNTIME_BASE_URL_TEMPLATE = "https://bedrock-runtime.{region}.amazonaws.com/openai/v1"
_MANTLE_DOCS_URL = "https://docs.aws.amazon.com/bedrock/latest/userguide/inference-openai.html"
_RUNTIME_REGIONS_DOCS_URL = "https://docs.aws.amazon.com/bedrock/latest/userguide/endpoints-region-availability.html"
_ENDPOINTS_DOCS_URL = "https://docs.aws.amazon.com/bedrock/latest/userguide/endpoints.html"
_DEFAULT_ENDPOINT: Literal["bedrock-mantle", "bedrock-runtime"] = "bedrock-mantle"
# Mantle model lines served from /openai/v1; every other Mantle model uses /v1, and the
# wrong base path fails with HTTP 400. The base path is a per-model property that no
# Mantle API reports, so these prefixes were verified against the ``us-east-1`` catalog on
# 2026-08-05. Scope each prefix to a single model line, never a vendor: one vendor's lines
# can split across base paths (``google.gemma-4-*`` is on /openai/v1, ``google.gemma-3-*``
# is on /v1). An unmatched new line falls through to /v1; the ``test_mantle_routing``
# integ test fails naming any id that routes wrong.
_OPENAI_PATH_MODEL_PREFIXES: tuple[str, ...] = ("openai.gpt-5.", "openai.gpt-6-", "xai.grok-4.", "google.gemma-4-")
def _resolve_mantle_base_path(model_id: str) -> str:
"""Resolve the Mantle base path for ``model_id``.
Models matching :data:`_OPENAI_PATH_MODEL_PREFIXES` are served from ``/openai/v1``;
other Mantle-routed models (e.g. ``openai.gpt-oss-*``, ``google.gemma-3-*``) use ``/v1``.
"""
if model_id.startswith(_OPENAI_PATH_MODEL_PREFIXES):
return "/openai/v1"
return "/v1"
class BedrockMantleConfig(TypedDict, total=False):
"""Config for routing an OpenAI-compatible client through Amazon Bedrock.
Attributes:
endpoint: Which Bedrock endpoint family serves the request: ``"bedrock-mantle"``
(the default) or ``"bedrock-runtime"``. Only ``bedrock-runtime`` accepts
cross-Region inference profile ids (``us.openai.*``, ``global.openai.*``). See
https://docs.aws.amazon.com/bedrock/latest/userguide/endpoints.html for the
full comparison.
region: AWS region hosting the Bedrock endpoint. If omitted, resolved
from ``boto_session`` (if provided) or the standard boto3 chain
(``AWS_REGION`` / ``AWS_DEFAULT_REGION`` / active profile / EC2 metadata).
A :class:`ValueError` is raised if none resolve.
boto_session: Optional :class:`boto3.Session` used to resolve the region when
``region`` is not provided. Useful for picking up a non-default profile
without exporting env vars.
credentials_provider: Optional botocore :class:`~botocore.credentials.CredentialProvider`
forwarded to ``provide_token``. Omit to let the token generator use the
standard AWS credential chain.
expiry: Optional ``timedelta`` for the bearer token's lifetime, forwarded to
``provide_token``. Defaults to the generator's built-in lifetime when
omitted.
"""
endpoint: Literal["bedrock-mantle", "bedrock-runtime"]
region: str
boto_session: boto3.Session
credentials_provider: CredentialProvider
expiry: timedelta
def _resolve_base_url(config: BedrockMantleConfig, region: str, model_id: str) -> str:
"""Resolve the OpenAI client's ``base_url`` for the configured endpoint family.
Raises:
ValueError: If ``endpoint`` is neither ``"bedrock-mantle"`` nor ``"bedrock-runtime"``.
"""
endpoint = config.get("endpoint", _DEFAULT_ENDPOINT)
if endpoint == "bedrock-runtime":
return _RUNTIME_BASE_URL_TEMPLATE.format(region=region)
if endpoint != "bedrock-mantle":
raise ValueError(
f"Unknown Bedrock endpoint '{endpoint}' in bedrock_mantle_config. "
f"Use 'bedrock-mantle' or 'bedrock-runtime'. See {_ENDPOINTS_DOCS_URL} for the difference."
)
return _MANTLE_BASE_URL_TEMPLATE.format(region=region, path=_resolve_mantle_base_path(model_id))
def _resolve_region(config: BedrockMantleConfig) -> str:
"""Resolve the AWS region, preferring explicit config then falling back to boto3.
The resolved region is validated before it is returned, since it is interpolated
into the Bedrock endpoint URL by the caller.
Raises:
ValueError: If no region can be resolved from the config, an attached session,
or the standard boto3 credential chain, or if the resolved region is not a
well-formed AWS region identifier.
"""
region = config.get("region")
if region:
return validate_region(region)
session = config.get("boto_session")
if session is not None and session.region_name:
return validate_region(str(session.region_name))
# ``boto3.Session()`` with no args reads ``AWS_REGION`` / ``AWS_DEFAULT_REGION``,
# the active profile, and falls back to EC2 instance metadata — the same chain
# :class:`BedrockModel` uses.
default_region = boto3.Session().region_name
if default_region:
return validate_region(str(default_region))
# The two endpoint families are available in different Regions, so point at the list
# for the one actually being used.
regions_docs_url = (
_RUNTIME_REGIONS_DOCS_URL
if config.get("endpoint", _DEFAULT_ENDPOINT) == "bedrock-runtime"
else _MANTLE_DOCS_URL
)
raise ValueError(
"Could not resolve an AWS region for Amazon Bedrock. Pass 'region' in "
"bedrock_mantle_config, attach a boto_session with a configured region, or set "
f"AWS_REGION in the environment. See {regions_docs_url} for supported regions."
)
def resolve_bedrock_client_args(
config: BedrockMantleConfig, client_args: dict[str, Any] | None = None, model_id: str = ""
) -> dict[str, Any]:
"""Resolve a ``BedrockMantleConfig`` (plus optional ``client_args``) into OpenAI client kwargs.
Mints a fresh bearer token on every call. Callers are expected to validate that
``client_args`` does not contain ``base_url`` or ``api_key`` before calling this
function (typically at ``__init__`` time for fail-fast behavior).
The ``endpoint`` key selects the endpoint family; on ``bedrock-mantle`` the ``model_id``
additionally selects the base path via :func:`_resolve_mantle_base_path`.
Raises:
ValueError: If no region can be resolved, or ``endpoint`` is not a known value.
ImportError: If ``aws-bedrock-token-generator`` is not installed.
RuntimeError: If token minting fails (e.g. missing AWS credentials).
"""
region = _resolve_region(config)
# Resolved before the token is minted so invalid input never costs a token round trip.
base_url = _resolve_base_url(config, region, model_id)
# ``aws-bedrock-token-generator`` is included in the ``openai`` extras group but not in
# ``litellm`` or ``sagemaker`` (which also depend on the ``openai`` package). The lazy
# import keeps those extras from hitting an ImportError at module load.
try:
from aws_bedrock_token_generator import provide_token
except ImportError as e:
raise ImportError(
"bedrock_mantle_config requires the 'aws-bedrock-token-generator' package. "
"Install it with: pip install strands-agents[openai]"
) from e
# Only forward kwargs the user set; provide_token rejects expiry=None.
token_kwargs: dict[str, Any] = {"region": region}
if "credentials_provider" in config:
token_kwargs["aws_credentials_provider"] = config["credentials_provider"]
if "expiry" in config:
token_kwargs["expiry"] = config["expiry"]
try:
token = provide_token(**token_kwargs)
except Exception as e:
raise RuntimeError(
f"Failed to mint an Amazon Bedrock bearer token for region '{region}'. "
"Verify your AWS credentials and network connectivity."
) from e
resolved: dict[str, Any] = dict(client_args or {})
resolved["base_url"] = base_url
resolved["api_key"] = token
return resolved