- 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 первым)
191 lines
8.9 KiB
Python
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
|