From a265a25f5cb91c445950fc72607eda34550d4f11 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 03:45:36 +0000 Subject: [PATCH 1/4] feat(anthropic): add base_url parameter to AnthropicClient and RawAnthropicClient Add base_url support to AnthropicSettings TypedDict, RawAnthropicClient, and AnthropicClient so users can point the client at Foundry or other Anthropic-compatible endpoints without having to construct AsyncAnthropic manually. - Add base_url field to AnthropicSettings (resolved from ANTHROPIC_BASE_URL env var) - Add base_url parameter to RawAnthropicClient.__init__ and pass it to AsyncAnthropic - Add base_url parameter to AnthropicClient.__init__ and forward to super - Add unit tests for base_url on both client classes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_anthropic/_chat_client.py | 26 +++++++++++++++++ .../anthropic/tests/test_anthropic_client.py | 28 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 1d1b978dd2..98c181f152 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -216,10 +216,12 @@ class AnthropicSettings(TypedDict, total=False): Keys: api_key: The Anthropic API key. chat_model: The Anthropic chat model. + base_url: Optional base URL for the Anthropic API endpoint. """ api_key: SecretString | None chat_model: str | None + base_url: str | None class RawAnthropicClient( @@ -248,6 +250,7 @@ def __init__( *, api_key: str | None = None, model: str | None = None, + base_url: str | None = None, anthropic_client: AnthropicAsyncClient | None = None, additional_beta_flags: list[str] | None = None, additional_properties: dict[str, Any] | None = None, @@ -259,6 +262,8 @@ def __init__( Keyword Args: api_key: The Anthropic API key to use for authentication. model: The model to use. + base_url: Optional base URL for the Anthropic API endpoint. Useful for Foundry or + other compatible deployments. Falls back to ``ANTHROPIC_BASE_URL`` env variable. anthropic_client: An existing Anthropic client to use. If not provided, one will be created. This can be used to further configure the client before passing it in. For instance if you need to set a different base_url for testing or private deployments. @@ -284,6 +289,13 @@ def __init__( api_key="your_anthropic_api_key", ) + # Or with a custom base URL (e.g. for Foundry-compatible endpoints) + client = RawAnthropicClient( + model="claude-sonnet-4-5-20250929", + api_key="your_anthropic_api_key", + base_url="https://custom-anthropic-endpoint.com", + ) + # Or loading from a .env file client = RawAnthropicClient(env_file_path="path/to/.env") @@ -316,12 +328,14 @@ class MyOptions(AnthropicChatOptions, total=False): env_prefix="ANTHROPIC_", api_key=api_key, chat_model=model, + base_url=base_url, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) api_key_secret = anthropic_settings.get("api_key") model_setting = anthropic_settings.get("chat_model") + base_url_setting = anthropic_settings.get("base_url") if anthropic_client is None: if api_key_secret is None: @@ -332,6 +346,7 @@ class MyOptions(AnthropicChatOptions, total=False): anthropic_client = AsyncAnthropic( api_key=api_key_secret.get_secret_value(), + base_url=base_url_setting, default_headers={"User-Agent": get_user_agent()}, ) @@ -1409,6 +1424,7 @@ def __init__( *, api_key: str | None = None, model: str | None = None, + base_url: str | None = None, anthropic_client: AnthropicAsyncClient | None = None, additional_beta_flags: list[str] | None = None, additional_properties: dict[str, Any] | None = None, @@ -1422,6 +1438,8 @@ def __init__( Keyword Args: api_key: The Anthropic API key to use for authentication. model: The model to use. + base_url: Optional base URL for the Anthropic API endpoint. Useful for Foundry or + other compatible deployments. Falls back to ``ANTHROPIC_BASE_URL`` env variable. anthropic_client: An existing Anthropic client to use. If not provided, one will be created. This can be used to further configure the client before passing it in. For instance if you need to set a different base_url for testing or private deployments. @@ -1448,6 +1466,13 @@ def __init__( api_key="your_anthropic_api_key", ) + # Or with a custom base URL (e.g. for Foundry-compatible endpoints) + client = AnthropicClient( + model="claude-sonnet-4-5-20250929", + api_key="your_anthropic_api_key", + base_url="https://custom-anthropic-endpoint.com", + ) + # Or loading from a .env file client = AnthropicClient(env_file_path="path/to/.env") @@ -1477,6 +1502,7 @@ class MyOptions(AnthropicChatOptions, total=False): super().__init__( api_key=api_key, model=model, + base_url=base_url, anthropic_client=anthropic_client, additional_beta_flags=additional_beta_flags, additional_properties=additional_properties, diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 945e5356a4..75a91ec5f8 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -149,6 +149,34 @@ def test_anthropic_client_init_auto_create_client( assert client.model == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"] +def test_anthropic_client_init_with_base_url( + anthropic_unit_test_env: dict[str, str], +) -> None: + """Test AnthropicClient accepts a base_url and passes it to the underlying AsyncAnthropic client.""" + custom_url = "https://custom-anthropic-endpoint.com" + client = AnthropicClient( + api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"], + model=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"], + base_url=custom_url, + ) + + assert custom_url in str(client.anthropic_client.base_url) + + +def test_raw_anthropic_client_init_with_base_url( + anthropic_unit_test_env: dict[str, str], +) -> None: + """Test RawAnthropicClient accepts a base_url and passes it to the underlying AsyncAnthropic client.""" + custom_url = "https://custom-anthropic-endpoint.com" + client = RawAnthropicClient( + api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"], + model=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"], + base_url=custom_url, + ) + + assert custom_url in str(client.anthropic_client.base_url) + + def test_anthropic_client_init_missing_api_key() -> None: """Test AnthropicClient initialization when API key is missing.""" with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load: From d31f1198d0ec81d109150f2a7dacc124940581c9 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 03:54:14 +0000 Subject: [PATCH 2/4] Python: Add `base_url` parameter to `AnthropicClient` and `RawAnthropicClient` Fixes #5683 --- .../packages/core/agent_framework/__init__.py | 2 +- .../packages/core/agent_framework/_skills.py | 16 ++++--------- .../packages/core/tests/core/test_skills.py | 24 ++++++++++++------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/python/packages/core/agent_framework/__init__.py b/python/packages/core/agent_framework/__init__.py index eb439c3543..4592f8c716 100644 --- a/python/packages/core/agent_framework/__init__.py +++ b/python/packages/core/agent_framework/__init__.py @@ -352,8 +352,8 @@ "ContinuationToken", "ConversationSplit", "ConversationSplitter", - "Default", "DeduplicatingSkillsSource", + "Default", "DelegatingSkillsSource", "Edge", "EdgeCondition", diff --git a/python/packages/core/agent_framework/_skills.py b/python/packages/core/agent_framework/_skills.py index 082c6f1b69..06612b4df0 100644 --- a/python/packages/core/agent_framework/_skills.py +++ b/python/packages/core/agent_framework/_skills.py @@ -446,14 +446,10 @@ async def run(self, skill: Skill, args: dict[str, Any] | None = None, **kwargs: """ if not isinstance(skill, FileSkill): raise TypeError( - f"File-based script '{self.name}' requires a FileSkill " - f"but received '{type(skill).__name__}'." + f"File-based script '{self.name}' requires a FileSkill but received '{type(skill).__name__}'." ) if self._runner is None: - raise ValueError( - f"Script '{self.name}' requires a runner. " - "Provide a script_runner for file-based scripts." - ) + raise ValueError(f"Script '{self.name}' requires a runner. Provide a script_runner for file-based scripts.") result = self._runner(skill, self, args) if inspect.isawaitable(result): return await result @@ -570,8 +566,7 @@ def _validate_skill_description(name: str, description: str) -> None: raise ValueError("Skill description cannot be empty.") if len(description) > MAX_DESCRIPTION_LENGTH: raise ValueError( - f"Skill '{name}' has an invalid description: " - f"Must be {MAX_DESCRIPTION_LENGTH} characters or fewer." + f"Skill '{name}' has an invalid description: Must be {MAX_DESCRIPTION_LENGTH} characters or fewer." ) @@ -1993,10 +1988,7 @@ def _get_validated_resource_path(skill_dir: str, resource_name: str) -> str: raise ValueError(f"Resource file '{resource_name}' not found in skill directory '{skill_dir}'.") if FileSkillsSource._has_symlink_in_path(resource_full_path, root_directory_path): - raise ValueError( - f"Resource file '{resource_name}' " - "has a symlink in its path; symlinks are not allowed." - ) + raise ValueError(f"Resource file '{resource_name}' has a symlink in its path; symlinks are not allowed.") return resource_full_path diff --git a/python/packages/core/tests/core/test_skills.py b/python/packages/core/tests/core/test_skills.py index 36906d55b8..8e8c6a8aed 100644 --- a/python/packages/core/tests/core/test_skills.py +++ b/python/packages/core/tests/core/test_skills.py @@ -1190,7 +1190,9 @@ async def get_user_data(**kwargs: Any) -> Any: provider = SkillsProvider([skill]) await _init_provider(provider) - result = await provider._read_skill_resource(_raw_skills(provider), "prog-skill", "get_user_data", auth_token="abc") + result = await provider._read_skill_resource( + _raw_skills(provider), "prog-skill", "get_user_data", auth_token="abc" + ) assert result == "data with token=abc" async def test_read_callable_resource_without_kwargs_ignores_extra_args(self) -> None: @@ -2059,6 +2061,7 @@ async def test_read_sync_function(self) -> None: async def test_read_async_function(self) -> None: """read() awaits an async function and returns its result.""" + async def get_data() -> str: return "async result" @@ -2068,6 +2071,7 @@ async def get_data() -> str: async def test_read_function_with_kwargs(self) -> None: """read() forwards kwargs to functions that accept them.""" + def get_config(**kwargs: Any) -> str: return f"user={kwargs.get('user_id')}" @@ -2077,6 +2081,7 @@ def get_config(**kwargs: Any) -> str: async def test_read_async_function_with_kwargs(self) -> None: """read() forwards kwargs to async functions that accept them.""" + async def get_config(**kwargs: Any) -> str: return f"user={kwargs.get('user_id')}" @@ -2086,6 +2091,7 @@ async def get_config(**kwargs: Any) -> str: async def test_read_function_without_kwargs_ignores_extra(self) -> None: """read() does not pass kwargs to functions that don't accept them.""" + def simple() -> str: return "fixed" @@ -2095,6 +2101,7 @@ def simple() -> str: async def test_read_function_raises_propagates(self) -> None: """read() propagates exceptions from the function.""" + def failing() -> str: raise RuntimeError("boom") @@ -2747,6 +2754,7 @@ async def async_func(x: int = 0) -> str: async def test_code_script_returns_object(self) -> None: """Code-defined scripts can return non-string objects.""" + def returns_dict() -> dict: return {"status": "ok", "value": 42} @@ -2855,8 +2863,8 @@ def process(**kwargs: Any) -> str: provider = SkillsProvider([skill]) await _init_provider(provider) - result = await provider._run_skill_script(_raw_skills(provider), - "my-skill", "process", args={"mode": "llm-value"}, mode="runtime-value" + result = await provider._run_skill_script( + _raw_skills(provider), "my-skill", "process", args={"mode": "llm-value"}, mode="runtime-value" ) assert "Error" in result @@ -2946,6 +2954,7 @@ async def test_require_script_approval_does_not_affect_other_tools(self) -> None async def test_code_script_exception_returns_error(self) -> None: """A code script function that raises should return an error string.""" + def failing_script() -> str: raise RuntimeError("Something went wrong") @@ -3170,6 +3179,7 @@ async def test_code_skill_no_scripts_element(self) -> None: async def test_code_skill_scripts_element_contains_parameters(self) -> None: """Scripts XML includes parameters schema when the function has typed parameters.""" + def analyze(query: str, limit: int = 10) -> str: return "result" @@ -3755,9 +3765,7 @@ async def test_file_source_with_script_runner(self, tmp_path: Path) -> None: ) (skill_dir / "run.py").write_text("print('hi')", encoding="utf-8") - source = DeduplicatingSkillsSource( - FileSkillsSource(str(tmp_path), script_runner=_noop_script_runner) - ) + source = DeduplicatingSkillsSource(FileSkillsSource(str(tmp_path), script_runner=_noop_script_runner)) provider = SkillsProvider(source) await _init_provider(provider) assert "my-skill" in _ctx(provider)[0] @@ -3798,9 +3806,7 @@ async def source_runner(skill: Any, script: Any, args: Any = None) -> str: call_log.append("source") return "source" - source = DeduplicatingSkillsSource( - FileSkillsSource(str(tmp_path), script_runner=source_runner) - ) + source = DeduplicatingSkillsSource(FileSkillsSource(str(tmp_path), script_runner=source_runner)) provider = SkillsProvider(source) await _init_provider(provider) From 38dd61f698a56d4bda858a9942d30b01efcffd65 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 04:00:34 +0000 Subject: [PATCH 3/4] test: add ANTHROPIC_BASE_URL env fallback tests for issue #5683 Add unit tests verifying that both AnthropicClient and RawAnthropicClient pick up base_url from the ANTHROPIC_BASE_URL environment variable via load_settings when base_url is not passed explicitly as a constructor arg. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../anthropic/tests/test_anthropic_client.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 75a91ec5f8..918dcd36c2 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -177,6 +177,40 @@ def test_raw_anthropic_client_init_with_base_url( assert custom_url in str(client.anthropic_client.base_url) +@pytest.mark.parametrize( + "override_env_param_dict", + [{"ANTHROPIC_BASE_URL": "https://env-base-url.example.com"}], + indirect=True, +) +def test_anthropic_client_init_base_url_from_env( + anthropic_unit_test_env: dict[str, str], +) -> None: + """Test AnthropicClient picks up base_url from ANTHROPIC_BASE_URL env variable when not passed explicitly.""" + client = AnthropicClient( + api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"], + model=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"], + ) + + assert anthropic_unit_test_env["ANTHROPIC_BASE_URL"] in str(client.anthropic_client.base_url) + + +@pytest.mark.parametrize( + "override_env_param_dict", + [{"ANTHROPIC_BASE_URL": "https://env-base-url.example.com"}], + indirect=True, +) +def test_raw_anthropic_client_init_base_url_from_env( + anthropic_unit_test_env: dict[str, str], +) -> None: + """Test RawAnthropicClient picks up base_url from ANTHROPIC_BASE_URL env variable when not passed explicitly.""" + client = RawAnthropicClient( + api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"], + model=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"], + ) + + assert anthropic_unit_test_env["ANTHROPIC_BASE_URL"] in str(client.anthropic_client.base_url) + + def test_anthropic_client_init_missing_api_key() -> None: """Test AnthropicClient initialization when API key is missing.""" with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load: From bb9d1312ed23e902d5a001282b680593c8fbb6b1 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 7 May 2026 04:12:03 +0000 Subject: [PATCH 4/4] test(anthropic): explicit base_url kwarg beats ANTHROPIC_BASE_URL env var (#5683) Add regression tests asserting that when both ANTHROPIC_BASE_URL is set in the environment *and* an explicit base_url kwarg is passed to AnthropicClient / RawAnthropicClient, the explicit kwarg wins. This closes the priority-ordering contract (explicit arg > env var) that the existing tests left implicit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../anthropic/tests/test_anthropic_client.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 918dcd36c2..0cfec3423c 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -211,6 +211,46 @@ def test_raw_anthropic_client_init_base_url_from_env( assert anthropic_unit_test_env["ANTHROPIC_BASE_URL"] in str(client.anthropic_client.base_url) +@pytest.mark.parametrize( + "override_env_param_dict", + [{"ANTHROPIC_BASE_URL": "https://env-base-url.example.com"}], + indirect=True, +) +def test_anthropic_client_init_explicit_base_url_wins_over_env( + anthropic_unit_test_env: dict[str, str], +) -> None: + """Test that an explicit base_url kwarg takes priority over ANTHROPIC_BASE_URL env variable.""" + explicit_url = "https://explicit-endpoint.example.com" + client = AnthropicClient( + api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"], + model=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"], + base_url=explicit_url, + ) + + assert explicit_url in str(client.anthropic_client.base_url) + assert anthropic_unit_test_env["ANTHROPIC_BASE_URL"] not in str(client.anthropic_client.base_url) + + +@pytest.mark.parametrize( + "override_env_param_dict", + [{"ANTHROPIC_BASE_URL": "https://env-base-url.example.com"}], + indirect=True, +) +def test_raw_anthropic_client_init_explicit_base_url_wins_over_env( + anthropic_unit_test_env: dict[str, str], +) -> None: + """Test that an explicit base_url kwarg takes priority over ANTHROPIC_BASE_URL env variable.""" + explicit_url = "https://explicit-endpoint.example.com" + client = RawAnthropicClient( + api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"], + model=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL"], + base_url=explicit_url, + ) + + assert explicit_url in str(client.anthropic_client.base_url) + assert anthropic_unit_test_env["ANTHROPIC_BASE_URL"] not in str(client.anthropic_client.base_url) + + def test_anthropic_client_init_missing_api_key() -> None: """Test AnthropicClient initialization when API key is missing.""" with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load: