Rebase onto upstream (a4d95fd) #12

Closed
wylab wants to merge 144 commits from rebase-onto-upstream into main
2 changed files with 133 additions and 6 deletions
Showing only changes of commit 81c647c28a - Show all commits
+32 -6
View File
@@ -199,22 +199,32 @@ class AnthropicOAuthProvider(LLMProvider):
def _convert_tools_to_anthropic(
self,
tools: list[dict[str, Any]] | None
tools: list[dict[str, Any]] | list[Any] | None
) -> list[dict[str, Any]] | None:
"""Convert tools to Anthropic API format.
Supports both function tools (custom) and native tools (Anthropic).
Function tools are converted to Anthropic format.
Native tools are passed through unchanged.
Tool objects (with to_params/to_schema methods) are converted to dicts.
"""
if not tools:
return None
anthropic_tools = []
for tool in tools:
if tool.get("type") == "function":
# Convert tool objects to dicts first
if hasattr(tool, 'to_params'): # Native Anthropic tool
tool_dict = tool.to_params()
elif hasattr(tool, 'to_schema'): # Function tool
tool_dict = tool.to_schema()
else:
tool_dict = tool # Already a dict
# Now process the dict
if tool_dict.get("type") == "function":
# Convert function tool format
func = tool["function"]
func = tool_dict["function"]
anthropic_tools.append({
"name": func["name"],
"description": func.get("description", ""),
@@ -223,7 +233,7 @@ class AnthropicOAuthProvider(LLMProvider):
else:
# Pass through native tool format as-is
# (bash_20250124, text_editor_20250728, computer_20251124, etc.)
anthropic_tools.append(tool)
anthropic_tools.append(tool_dict)
return anthropic_tools if anthropic_tools else None
@@ -237,6 +247,7 @@ class AnthropicOAuthProvider(LLMProvider):
tools: list[dict[str, Any]] | None = None,
thinking_budget_override: int | None = None,
context_management: dict[str, Any] | None = None,
beta_flags: set[str] | None = None,
) -> dict[str, Any]:
"""Make request to Anthropic API."""
client = await self._get_client()
@@ -294,9 +305,14 @@ class AnthropicOAuthProvider(LLMProvider):
edit_types or "none",
)
# Build headers with beta flags if provided
headers = self._get_headers()
if beta_flags:
headers["anthropic-beta"] = ",".join(sorted(beta_flags))
response = await client.post(
self._get_api_url(),
headers=self._get_headers(),
headers=headers,
json=payload,
)
@@ -342,7 +358,7 @@ class AnthropicOAuthProvider(LLMProvider):
async def chat(
self,
messages: list[dict[str, Any]],
tools: list[dict[str, Any]] | None = None,
tools: list[dict[str, Any]] | list[Any] | None = None,
model: str | None = None,
max_tokens: int = 4096,
temperature: float = 0.7,
@@ -360,6 +376,15 @@ class AnthropicOAuthProvider(LLMProvider):
model = self._normalize_model(model)
system, prepared_messages = self._prepare_messages(messages)
# Collect beta flags from native tools BEFORE conversion
beta_flags: set[str] = set()
if tools:
for tool in tools:
if hasattr(tool, 'beta_flag') and tool.beta_flag:
beta_flags.add(tool.beta_flag)
# Convert tools to API format
anthropic_tools = self._convert_tools_to_anthropic(tools)
# Per-call thinking override (None = use instance default)
@@ -375,6 +400,7 @@ class AnthropicOAuthProvider(LLMProvider):
tools=anthropic_tools,
thinking_budget_override=effective_thinking,
context_management=context_management,
beta_flags=beta_flags,
)
return self._parse_response(response)
except Exception as e:
+101
View File
@@ -0,0 +1,101 @@
"""Tests for beta flag collection from native tools."""
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from nanobot.providers.anthropic_oauth import AnthropicOAuthProvider
@pytest.mark.asyncio
async def test_beta_flags_collected_from_tools():
"""Test that beta flags are extracted from tool objects."""
provider = AnthropicOAuthProvider(oauth_token="test", thinking_budget=0)
# Mock tool objects with beta_flag attribute and to_params method
class MockTool:
def __init__(self, beta_flag):
self.beta_flag = beta_flag
def to_params(self):
return {"type": "bash_20250124", "name": "bash"}
tools_with_flags = [
MockTool("computer-use-2025-11-24"),
MockTool("computer-use-2025-11-24"), # Duplicate should be deduplicated
]
# We need to test this via the actual API call flow
# Mock httpx client
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "msg_test",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "test"}],
"model": "claude-opus-4",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 10}
}
with patch.object(provider, '_client') as mock_client:
mock_client.post = AsyncMock(return_value=mock_response)
# Call with messages and tools
await provider.chat(
messages=[{"role": "user", "content": "test"}],
model="claude-opus-4",
max_tokens=100,
tools=tools_with_flags
)
# Check that beta flag was added to headers
call_args = mock_client.post.call_args
headers = call_args[1]["headers"]
assert "anthropic-beta" in headers
assert headers["anthropic-beta"] == "computer-use-2025-11-24"
@pytest.mark.asyncio
async def test_multiple_beta_flags_joined():
"""Test that multiple unique beta flags are joined with commas."""
provider = AnthropicOAuthProvider(oauth_token="test", thinking_budget=0)
class MockTool:
def __init__(self, beta_flag):
self.beta_flag = beta_flag
def to_params(self):
return {"type": "bash_20250124", "name": "bash"}
tools_with_flags = [
MockTool("flag-a"),
MockTool("flag-b"),
]
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"id": "msg_test",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "test"}],
"model": "claude-opus-4",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 10}
}
with patch.object(provider, '_client') as mock_client:
mock_client.post = AsyncMock(return_value=mock_response)
await provider.chat(
messages=[{"role": "user", "content": "test"}],
model="claude-opus-4",
max_tokens=100,
tools=tools_with_flags
)
call_args = mock_client.post.call_args
headers = call_args[1]["headers"]
assert "anthropic-beta" in headers
# Should be sorted and joined
assert headers["anthropic-beta"] in ["flag-a,flag-b", "flag-b,flag-a"]