diff --git a/README.md b/README.md index 8858ebb5..4385d321 100644 --- a/README.md +++ b/README.md @@ -853,6 +853,18 @@ result = agent.tool.use_agent( "params": {"temperature": 1, "max_tokens": 4000} } ) + +# Use OrcaRouter as a named provider (OpenAI-compatible gateway) +os.environ["ORCAROUTER_API_KEY"] = "sk-orca-..." +result = agent.tool.use_agent( + prompt="Analyze this code", + system_prompt="You are a code review assistant.", + model_provider="orcarouter", + model_settings={ + "model_id": "orcarouter/auto", + "params": {"temperature": 1, "max_tokens": 4000} + } +) ``` ### A2A Client @@ -1401,6 +1413,8 @@ The Mem0 Memory Tool supports three different backend configurations: | STRANDS_MODEL_ID | Default model identifier for environment-based model selection | None | | STRANDS_MAX_TOKENS | Maximum tokens for the nested agent model | None | | STRANDS_TEMPERATURE | Sampling temperature for the nested agent model | None | +| ORCAROUTER_API_KEY | API key for the OrcaRouter provider | None | +| ORCAROUTER_BASE_URL | Base URL for the OrcaRouter provider | https://api.orcarouter.ai/v1 | #### Elasticsearch Memory Tool diff --git a/src/strands_tools/think.py b/src/strands_tools/think.py index 76c7f274..e692827f 100644 --- a/src/strands_tools/think.py +++ b/src/strands_tools/think.py @@ -264,7 +264,7 @@ def think( exist in the parent agent's tool registry. Examples: ["calculator", "file_read", "retrieve"] If not provided, inherits all tools from the parent agent. model_provider: Model provider to use for the thinking cycles. - Options: "bedrock", "anthropic", "litellm", "llamaapi", "ollama", "openai", "github" + Options: "bedrock", "anthropic", "litellm", "llamaapi", "ollama", "openai", "github", "orcarouter" Special values: - None: Use parent agent's model (default, preserves original behavior) - "env": Use environment variables to determine provider diff --git a/src/strands_tools/use_agent.py b/src/strands_tools/use_agent.py index e6eca638..1b0b4f08 100644 --- a/src/strands_tools/use_agent.py +++ b/src/strands_tools/use_agent.py @@ -114,7 +114,7 @@ def use_agent( Examples: ["calculator", "file_read", "retrieve"] If not provided, inherits all tools from the parent agent. model_provider: Model provider to use for the nested agent. - Options: "bedrock", "anthropic", "litellm", "llamaapi", "ollama", "openai", "github" + Options: "bedrock", "anthropic", "litellm", "llamaapi", "ollama", "openai", "github", "orcarouter" Special values: - None: Use parent agent's model (default, preserves original behavior) - "env": Use environment variables to determine provider diff --git a/src/strands_tools/utils/models/__init__.py b/src/strands_tools/utils/models/__init__.py index 80577bd6..8a798793 100644 --- a/src/strands_tools/utils/models/__init__.py +++ b/src/strands_tools/utils/models/__init__.py @@ -12,4 +12,5 @@ "writer", "cohere", "openai", + "orcarouter", ] diff --git a/src/strands_tools/utils/models/model.py b/src/strands_tools/utils/models/model.py index b8d3d9af..1338f302 100644 --- a/src/strands_tools/utils/models/model.py +++ b/src/strands_tools/utils/models/model.py @@ -173,6 +173,11 @@ def create_model(provider: str = None, config: dict[str, Any] = None) -> Model: return OpenAIModel(**config) + elif provider == "orcarouter": + from strands.models.openai import OpenAIModel + + return OpenAIModel(**config) + else: # Try to load custom model provider try: @@ -284,6 +289,16 @@ def get_provider_config(provider: str) -> dict[str, Any]: "params": {"max_tokens": int(os.getenv("STRANDS_MAX_TOKENS", "4000"))}, } + elif provider == "orcarouter": + return { + "client_args": { + "api_key": os.getenv("ORCAROUTER_API_KEY"), + "base_url": os.getenv("ORCAROUTER_BASE_URL", "https://api.orcarouter.ai/v1"), + }, + "model_id": os.getenv("STRANDS_MODEL_ID", "orcarouter/auto"), + "params": {"max_tokens": int(os.getenv("STRANDS_MAX_TOKENS", "4000"))}, + } + else: raise ValueError(f"Unknown provider: {provider}") @@ -304,6 +319,7 @@ def get_available_providers() -> list[str]: "writer", "cohere", "github", + "orcarouter", ] @@ -391,6 +407,17 @@ def get_provider_info(provider: str) -> dict[str, Any]: "STRANDS_MAX_TOKENS", ], }, + "orcarouter": { + "name": "OrcaRouter", + "description": "Unified OpenAI-compatible gateway with zero-trust security for AI agents", + "default_model": "orcarouter/auto", + "env_vars": [ + "ORCAROUTER_API_KEY", + "ORCAROUTER_BASE_URL", + "STRANDS_MODEL_ID", + "STRANDS_MAX_TOKENS", + ], + }, } return provider_info.get(provider, {"name": provider, "description": "Custom provider"}) diff --git a/src/strands_tools/workflow.py b/src/strands_tools/workflow.py index c9dd0555..26bcec46 100644 --- a/src/strands_tools/workflow.py +++ b/src/strands_tools/workflow.py @@ -978,7 +978,7 @@ def workflow( • system_prompt (str): Custom system prompt for this task [OPTIONAL] • tools (List[str]): Tool names available to this task [OPTIONAL] • model_provider (str): Model provider for this task [OPTIONAL] - Options: "bedrock", "anthropic", "ollama", "openai", "github", "env" + Options: "bedrock", "anthropic", "ollama", "openai", "github", "orcarouter", "env" • model_settings (Dict): Model configuration [OPTIONAL] Example: {"model_id": "claude-sonnet-4", "params": {"temperature": 0.7}} • dependencies (List[str]): Task IDs this task depends on [OPTIONAL] diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 00000000..11942767 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,43 @@ +"""Tests for the model provider registry (strands_tools.utils.models.model).""" + +from unittest.mock import patch + +from strands_tools.utils.models.model import ( + create_model, + get_available_providers, + get_provider_config, + get_provider_info, +) + + +def test_get_provider_config_orcarouter_defaults(): + """OrcaRouter provider should default to the OrcaRouter gateway base URL and auto model.""" + config = get_provider_config("orcarouter") + assert config["client_args"]["base_url"] == "https://api.orcarouter.ai/v1" + assert config["model_id"] == "orcarouter/auto" + + +def test_get_provider_config_orcarouter_env_override(monkeypatch): + """ORCAROUTER_API_KEY and ORCAROUTER_BASE_URL should be honored.""" + monkeypatch.setenv("ORCAROUTER_API_KEY", "sk-orca-test") + monkeypatch.setenv("ORCAROUTER_BASE_URL", "https://custom.example/v1") + config = get_provider_config("orcarouter") + assert config["client_args"]["api_key"] == "sk-orca-test" + assert config["client_args"]["base_url"] == "https://custom.example/v1" + + +def test_get_available_providers_includes_orcarouter(): + assert "orcarouter" in get_available_providers() + + +def test_get_provider_info_orcarouter(): + info = get_provider_info("orcarouter") + assert info["name"] == "OrcaRouter" + assert "ORCAROUTER_API_KEY" in info["env_vars"] + + +def test_create_model_orcarouter_uses_openai_model(): + """The orcarouter provider is OpenAI-compatible and should build an OpenAIModel.""" + with patch("strands.models.openai.OpenAIModel") as mock_cls: + create_model("orcarouter", {"model_id": "orcarouter/auto"}) + mock_cls.assert_called_once_with(model_id="orcarouter/auto") diff --git a/tests/test_use_agent.py b/tests/test_use_agent.py index 4f54eeb8..acd8be30 100644 --- a/tests/test_use_agent.py +++ b/tests/test_use_agent.py @@ -408,6 +408,7 @@ def test_use_agent_with_all_model_providers(mock_parent_agent, mock_agent_result "ollama", "openai", "github", + "orcarouter", ] for provider in providers: