Skip to content
Merged
166 changes: 162 additions & 4 deletions src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,7 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
"python/http/strands/base/mcp_client/client.py",
"python/http/strands/base/model/__init__.py",
"python/http/strands/base/model/load.py",
"python/http/strands/base/model/mantle_compat.py",
"python/http/strands/base/pyproject.toml",
"python/http/strands/base/skills/fetcher.py",
"python/http/strands/capabilities/execution-limits/hooks/execution_limits.py",
Expand Down Expand Up @@ -5276,7 +5277,7 @@ from mcp_client.client import get_streamable_http_mcp_client
from memory.session import get_memory_session_manager
{{/if}}
{{#unless hasFileOperations}}
{{#if (or needsOs (some gitSkills "credentialArn"))}}
{{#if (or needsOs browserIdentifierEnvVar codeInterpreterIdentifierEnvVar (some gitSkills "credentialArn"))}}
import os
{{/if}}
{{/unless}}
Expand Down Expand Up @@ -5362,10 +5363,20 @@ tools.append(add_numbers)
{{/unless}}
{{/if}}
{{#if hasBrowser}}
tools.append(AgentCoreBrowser({{#if browserIdentifier}}identifier="{{browserIdentifier}}"{{/if}}).browser)
{{#if browserIdentifierEnvVar}}
_browser_id = os.getenv("{{browserIdentifierEnvVar}}")
tools.append(AgentCoreBrowser(**({"identifier": _browser_id} if _browser_id else {})).browser)
{{else}}
tools.append(AgentCoreBrowser().browser)
{{/if}}
{{/if}}
{{#if hasCodeInterpreter}}
tools.append(AgentCoreCodeInterpreter({{#if codeInterpreterIdentifier}}identifier="{{codeInterpreterIdentifier}}"{{/if}}).code_interpreter)
{{#if codeInterpreterIdentifierEnvVar}}
_code_interpreter_id = os.getenv("{{codeInterpreterIdentifierEnvVar}}")
tools.append(AgentCoreCodeInterpreter(**({"identifier": _code_interpreter_id} if _code_interpreter_id else {})).code_interpreter)
{{else}}
tools.append(AgentCoreCodeInterpreter().code_interpreter)
{{/if}}
{{/if}}
{{#if hasShell}}
@tool
Expand Down Expand Up @@ -5908,7 +5919,10 @@ from bedrock_agentcore.identity import requires_access_token
@requires_access_token(
provider_name="{{credentialProviderName}}",
scopes=[{{#if scopes}}"{{scopes}}"{{/if}}],
auth_flow="M2M",
auth_flow="{{#if authFlow}}{{authFlow}}{{else}}M2M{{/if}}",
{{#if customParameters}}
custom_parameters={{safeJson customParameters}},
{{/if}}
)
def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
"""Obtain OAuth access token via AgentCore Identity for {{name}}."""
Expand Down Expand Up @@ -6011,13 +6025,75 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ht

exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/model/load.py should match snapshot 1`] = `
"{{#if (eq modelProvider "Bedrock")}}
{{#if bedrockMantle}}
import os

from aws_bedrock_token_generator import provide_token
{{#if (eq mantleApiFormat "chat_completions")}}
from strands.models.openai import OpenAIModel
{{else}}
{{#if mantleProprietary}}
from strands.models.openai_responses import OpenAIResponsesModel
{{else}}
from model.mantle_compat import MantleCompatResponsesModel
{{/if}}
{{/if}}

MODEL_ID = "{{modelId}}"


def load_model():
"""
Get a Bedrock Mantle model client. These OpenAI-compatible models (e.g. openai.gpt-5.5,
openai.gpt-oss-120b) are served via the Bedrock Mantle endpoint, NOT the Converse API — so they
are invoked through an OpenAI-style client authenticated with a short-lived Bedrock bearer token.
Region is read from AWS_REGION (set by the AgentCore runtime).
"""
region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1"))
token = provide_token(region=region)
{{#if mantleProprietary}}
# Proprietary OpenAI models only work on the /openai/v1 Mantle path.
base_url = f"https://bedrock-mantle.{region}.api.aws/openai/v1"
{{else}}
# Open-source OpenAI models (gpt-oss-*) only work on the /v1 Mantle path.
base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
{{/if}}
client_args = {"api_key": token, "base_url": base_url}

params = {}
{{#if modelMaxTokens}}
{{#if (eq mantleApiFormat "chat_completions")}}
params["max_completion_tokens"] = {{modelMaxTokens}}
{{else}}
params["max_output_tokens"] = {{modelMaxTokens}}
{{/if}}
{{/if}}
{{#if modelTemperature}}
params["temperature"] = {{modelTemperature}}
{{/if}}
{{#if modelTopP}}
params["top_p"] = {{modelTopP}}
{{/if}}
{{#if (eq mantleApiFormat "chat_completions")}}
return OpenAIModel(client_args=client_args, model_id=MODEL_ID, params=params)
{{else}}
# Responses API: Mantle does not persist responses, so disable server-side storage.
params["store"] = False
{{#if mantleProprietary}}
return OpenAIResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
{{else}}
return MantleCompatResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
{{/if}}
{{/if}}
{{else}}
from strands.models.bedrock import BedrockModel


def load_model() -> BedrockModel:
"""Get Bedrock model client using IAM credentials."""
return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}")
{{/if}}
{{/if}}
{{#if (eq modelProvider "Anthropic")}}
import os

Expand Down Expand Up @@ -6133,6 +6209,85 @@ def load_model() -> GeminiModel:
model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
)
{{/if}}
{{#if (eq modelProvider "LiteLLM")}}
import os
{{#if litellmAdditionalParams}}
import json
{{/if}}

from strands.models.litellm import LiteLLMModel
{{#if identityProviders.[0].name}}
from bedrock_agentcore.identity.auth import requires_api_key

IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}"
IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}"


@requires_api_key(provider_name=IDENTITY_PROVIDER_NAME)
def _agentcore_identity_api_key_provider(api_key: str) -> str:
"""Fetch API key from AgentCore Identity."""
return api_key


def _get_api_key() -> str:
"""
Uses AgentCore Identity for API key management in deployed environments.
For local development, run via 'agentcore dev' which loads agentcore/.env.
"""
if os.getenv("LOCAL_DEV") == "1":
api_key = os.getenv(IDENTITY_ENV_VAR)
if not api_key:
raise RuntimeError(
f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local"
)
return api_key
return _agentcore_identity_api_key_provider()
{{/if}}




def load_model() -> LiteLLMModel:
"""Get a LiteLLM model client (proxies to the provider encoded in model_id)."""
client_args = {}
{{#if identityProviders.[0].name}}
client_args["api_key"] = _get_api_key()
{{/if}}
{{#if litellmApiBase}}
client_args["api_base"] = {{safeJson litellmApiBase}}
{{/if}}
params = {{#if litellmAdditionalParams}}json.loads({{pyJsonStr litellmAdditionalParams}}){{else}}{}{{/if}}
return LiteLLMModel(
client_args=client_args,
model_id="{{#if modelId}}{{modelId}}{{else}}bedrock/us.anthropic.claude-sonnet-4-5-20250514-v1:0{{/if}}",
params=params,
)
{{/if}}
"
`;

exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/model/mantle_compat.py should match snapshot 1`] = `
"from strands.models.openai_responses import OpenAIResponsesModel


class MantleCompatResponsesModel(OpenAIResponsesModel):
"""Workaround for Bedrock Mantle rejecting output_text in EasyInputMessage content arrays.

Mantle's Pydantic validation only accepts content as a plain string for assistant messages, while
real OpenAI accepts both formats. Flatten assistant content arrays to strings so multi-turn works.
Used for open-source OpenAI models (gpt-oss-*) on the /v1 Mantle path; proprietary models use the
plain OpenAIResponsesModel on /openai/v1.
"""

@classmethod
def _format_request_messages(cls, messages):
formatted = super()._format_request_messages(messages)
for msg in formatted:
if msg.get("role") == "assistant" and isinstance(msg.get("content"), list):
msg["content"] = "".join(
part.get("text", "") for part in msg["content"] if part.get("type") == "output_text"
)
return formatted
"
`;

Expand All @@ -6155,6 +6310,9 @@ dependencies = [
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
{{/if}}"mcp >= 1.19.0",
{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
{{/if}}{{#if (eq modelProvider "LiteLLM")}}"litellm >= 1.0.0",
{{/if}}{{#if bedrockMantle}}"openai >= 1.0.0",
"aws-bedrock-token-generator >= 1.0.0",
{{/if}}"strands-agents >= 1.15.0",
{{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools >= 0.1.0",
{{/if}}{{#if hasBrowser}}"nest-asyncio >= 1.5.0",
Expand Down
16 changes: 13 additions & 3 deletions src/assets/python/http/strands/base/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
from memory.session import get_memory_session_manager
{{/if}}
{{#unless hasFileOperations}}
{{#if (or needsOs (some gitSkills "credentialArn"))}}
{{#if (or needsOs browserIdentifierEnvVar codeInterpreterIdentifierEnvVar (some gitSkills "credentialArn"))}}
import os
{{/if}}
{{/unless}}
Expand Down Expand Up @@ -152,10 +152,20 @@ def add_numbers(a: int, b: int) -> int:
{{/unless}}
{{/if}}
{{#if hasBrowser}}
tools.append(AgentCoreBrowser({{#if browserIdentifier}}identifier="{{browserIdentifier}}"{{/if}}).browser)
{{#if browserIdentifierEnvVar}}
_browser_id = os.getenv("{{browserIdentifierEnvVar}}")
tools.append(AgentCoreBrowser(**({"identifier": _browser_id} if _browser_id else {})).browser)
{{else}}
tools.append(AgentCoreBrowser().browser)
{{/if}}
{{/if}}
{{#if hasCodeInterpreter}}
tools.append(AgentCoreCodeInterpreter({{#if codeInterpreterIdentifier}}identifier="{{codeInterpreterIdentifier}}"{{/if}}).code_interpreter)
{{#if codeInterpreterIdentifierEnvVar}}
_code_interpreter_id = os.getenv("{{codeInterpreterIdentifierEnvVar}}")
tools.append(AgentCoreCodeInterpreter(**({"identifier": _code_interpreter_id} if _code_interpreter_id else {})).code_interpreter)
{{else}}
tools.append(AgentCoreCodeInterpreter().code_interpreter)
{{/if}}
{{/if}}
{{#if hasShell}}
@tool
Expand Down
5 changes: 4 additions & 1 deletion src/assets/python/http/strands/base/mcp_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
@requires_access_token(
provider_name="{{credentialProviderName}}",
scopes=[{{#if scopes}}"{{scopes}}"{{/if}}],
auth_flow="M2M",
auth_flow="{{#if authFlow}}{{authFlow}}{{else}}M2M{{/if}}",
{{#if customParameters}}
custom_parameters={{safeJson customParameters}},
{{/if}}
)
def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
"""Obtain OAuth access token via AgentCore Identity for {{name}}."""
Expand Down
116 changes: 116 additions & 0 deletions src/assets/python/http/strands/base/model/load.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,73 @@
{{#if (eq modelProvider "Bedrock")}}
{{#if bedrockMantle}}
import os

from aws_bedrock_token_generator import provide_token
{{#if (eq mantleApiFormat "chat_completions")}}
from strands.models.openai import OpenAIModel
{{else}}
{{#if mantleProprietary}}
from strands.models.openai_responses import OpenAIResponsesModel
{{else}}
from model.mantle_compat import MantleCompatResponsesModel
{{/if}}
{{/if}}

MODEL_ID = "{{modelId}}"


def load_model():
"""
Get a Bedrock Mantle model client. These OpenAI-compatible models (e.g. openai.gpt-5.5,
openai.gpt-oss-120b) are served via the Bedrock Mantle endpoint, NOT the Converse API — so they
are invoked through an OpenAI-style client authenticated with a short-lived Bedrock bearer token.
Region is read from AWS_REGION (set by the AgentCore runtime).
"""
region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1"))
token = provide_token(region=region)
{{#if mantleProprietary}}
# Proprietary OpenAI models only work on the /openai/v1 Mantle path.
base_url = f"https://bedrock-mantle.{region}.api.aws/openai/v1"
{{else}}
# Open-source OpenAI models (gpt-oss-*) only work on the /v1 Mantle path.
base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
{{/if}}
client_args = {"api_key": token, "base_url": base_url}

params = {}
{{#if modelMaxTokens}}
{{#if (eq mantleApiFormat "chat_completions")}}
params["max_completion_tokens"] = {{modelMaxTokens}}
{{else}}
params["max_output_tokens"] = {{modelMaxTokens}}
{{/if}}
{{/if}}
{{#if modelTemperature}}
params["temperature"] = {{modelTemperature}}
{{/if}}
{{#if modelTopP}}
params["top_p"] = {{modelTopP}}
{{/if}}
{{#if (eq mantleApiFormat "chat_completions")}}
return OpenAIModel(client_args=client_args, model_id=MODEL_ID, params=params)
{{else}}
# Responses API: Mantle does not persist responses, so disable server-side storage.
params["store"] = False
{{#if mantleProprietary}}
return OpenAIResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
{{else}}
return MantleCompatResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
{{/if}}
{{/if}}
{{else}}
from strands.models.bedrock import BedrockModel


def load_model() -> BedrockModel:
"""Get Bedrock model client using IAM credentials."""
return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}")
{{/if}}
{{/if}}
{{#if (eq modelProvider "Anthropic")}}
import os

Expand Down Expand Up @@ -121,3 +183,57 @@ def load_model() -> GeminiModel:
model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
)
{{/if}}
{{#if (eq modelProvider "LiteLLM")}}
import os
{{#if litellmAdditionalParams}}
import json
{{/if}}

from strands.models.litellm import LiteLLMModel
{{#if identityProviders.[0].name}}
from bedrock_agentcore.identity.auth import requires_api_key

IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}"
IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}"


@requires_api_key(provider_name=IDENTITY_PROVIDER_NAME)
def _agentcore_identity_api_key_provider(api_key: str) -> str:
"""Fetch API key from AgentCore Identity."""
return api_key


def _get_api_key() -> str:
"""
Uses AgentCore Identity for API key management in deployed environments.
For local development, run via 'agentcore dev' which loads agentcore/.env.
"""
if os.getenv("LOCAL_DEV") == "1":
api_key = os.getenv(IDENTITY_ENV_VAR)
if not api_key:
raise RuntimeError(
f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local"
)
return api_key
return _agentcore_identity_api_key_provider()
{{/if}}




def load_model() -> LiteLLMModel:
"""Get a LiteLLM model client (proxies to the provider encoded in model_id)."""
client_args = {}
{{#if identityProviders.[0].name}}
client_args["api_key"] = _get_api_key()
{{/if}}
{{#if litellmApiBase}}
client_args["api_base"] = {{safeJson litellmApiBase}}
{{/if}}
params = {{#if litellmAdditionalParams}}json.loads({{pyJsonStr litellmAdditionalParams}}){{else}}{}{{/if}}
return LiteLLMModel(
client_args=client_args,
model_id="{{#if modelId}}{{modelId}}{{else}}bedrock/us.anthropic.claude-sonnet-4-5-20250514-v1:0{{/if}}",
params=params,
)
{{/if}}
Loading
Loading