Python: Add base_url parameter to AnthropicClient and RawAnthropicClient (#5685)

* 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>

* Python: Add `base_url` parameter to `AnthropicClient` and `RawAnthropicClient`

Fixes #5683

* 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>

* 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>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Evan Mattson
2026-05-08 02:57:09 +09:00
committed by GitHub
Unverified
parent 44381c051b
commit 8bb4692678
5 changed files with 148 additions and 22 deletions
@@ -352,8 +352,8 @@ __all__ = [
"ContinuationToken",
"ConversationSplit",
"ConversationSplitter",
"Default",
"DeduplicatingSkillsSource",
"Default",
"DelegatingSkillsSource",
"Edge",
"EdgeCondition",
@@ -446,14 +446,10 @@ class FileSkillScript(SkillScript):
"""
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 @@ class FileSkillsSource(SkillsSource):
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
+15 -9
View File
@@ -1190,7 +1190,9 @@ class TestSkillsProviderCodeSkill:
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 @@ class TestSkillResourceRead:
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 @@ class TestSkillResourceRead:
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 @@ class TestSkillResourceRead:
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 @@ class TestSkillResourceRead:
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 @@ class TestSkillResourceRead:
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 @@ class TestSkillsProviderFactories:
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 @@ class TestSkillsProviderFactories:
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 @@ class TestSkillsProviderFactories:
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 @@ class TestLoadSkillWithScripts:
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 @@ class TestSourceComposition:
)
(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 @@ class TestSourceComposition:
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)