diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98c045960..8814146c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -215,3 +215,9 @@ jobs: --ignore=tests/unit/test_transcript_action_workflow.py -k "not integration" - name: Offline MCP inventory and approval regressions run: python -m pytest tests/security/test_local_mcp_config.py tests/security/test_mcp_catalog_approval.py -q -o addopts='' + - name: Anthropic SDK 2 client compatibility + run: >- + PYTHONPATH=src uv run --no-project --python "${{ matrix.python-version }}" + --with pytest --with anthropic==2.0.0 --with httpx2 + --with aiohttp --with pydantic + python -m pytest tests/compat/test_anthropic_v2_compat.py -q -o addopts='' diff --git a/pyproject.toml b/pyproject.toml index 325701cea..116d1e566 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,7 @@ ml = [ "tokenizers>=0.15.0", "sentence-transformers>=2.2.0", "openai>=1.3.0", - "anthropic>=0.105.0,<2", + "anthropic>=0.105.0,<3", "google-generativeai>=0.3.0", "cohere>=4.37.0", "numpy>=1.24.0", diff --git a/tests/compat/test_anthropic_v2_compat.py b/tests/compat/test_anthropic_v2_compat.py new file mode 100644 index 000000000..b044d24af --- /dev/null +++ b/tests/compat/test_anthropic_v2_compat.py @@ -0,0 +1,81 @@ +import asyncio +import json + +import pytest + +anthropic = pytest.importorskip("anthropic", minversion="2.0") +httpx = pytest.importorskip("httpx2") + +from agents.mcp_tools.tri_model_consensus_tool import TriModelConsensusTool +from unified_ai_sdk import AIRequest, ModelProvider, TaskType, UnifiedAISDK +from youtube_extension.core.mcp.context_manager import MCPContext +from youtube_extension.core.mcp.protocol_bridge import AnthropicAdapter + +calls = [] + + +def respond(request: httpx.Request) -> httpx.Response: + payload = json.loads(request.content) + calls.append((request.url.path, request.headers.get("anthropic-version"), payload)) + if request.url.path.endswith("/count_tokens"): + return httpx.Response(200, json={"input_tokens": 1}) + return httpx.Response( + 200, + json={ + "id": "msg_test", + "type": "message", + "role": "assistant", + "content": [{"type": "text", "text": "mocked response"}], + "model": "claude-opus-4-8", + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 11, "output_tokens": 22}, + }, + ) + + +async def main(): + async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as http_client: + client = anthropic.AsyncAnthropic( + api_key="sk-ant-test", http_client=http_client + ) + sdk = UnifiedAISDK({"anthropic_api_key": "sk-ant-test", "retry_attempts": 1}) + sdk._anthropic_client = client + result = await sdk.unified_request( + AIRequest( + prompt="Summarize this", + model="claude-opus-4-8", + provider=ModelProvider.CLAUDE, + task_type=TaskType.SUMMARIZATION, + ) + ) + assert ( + result.success + and result.content == "mocked response" + and result.tokens_used == 33 + ) + + adapter = AnthropicAdapter() + assert await adapter.initialize({"api_key": "sk-ant-test"}) + adapter._client = client + context = MCPContext(user="test", task="compat", intent="test Anthropic v2") + adapter_result = await adapter.send_request({"prompt": "hello"}, context) + assert adapter_result["content"] == "mocked response" + assert adapter_result["usage"] == {"input_tokens": 11, "output_tokens": 22} + assert await adapter.health_check() + + consensus = object.__new__(TriModelConsensusTool) + consensus.claude_client = client + consensus_result = await consensus._query_claude("reason", "generic") + assert ( + consensus_result.response == "mocked response" + and not consensus_result.error + ) + + assert len(calls) == 4 and all(version == "2026-10-21" for _, version, _ in calls) + assert calls[2][0].endswith("/count_tokens") + assert anthropic.__version__.startswith("2.") + + +def test_anthropic_v2_app_callsites(): + asyncio.run(main()) diff --git a/uv.lock b/uv.lock index 631beeb5c..9c5f3bbfd 100644 --- a/uv.lock +++ b/uv.lock @@ -5671,7 +5671,7 @@ requires-dist = [ { name = "aiohttp", specifier = ">=3.8.0" }, { name = "aiosqlite", specifier = ">=0.19.0" }, { name = "alembic", specifier = ">=1.12.0" }, - { name = "anthropic", marker = "extra == 'ml'", specifier = ">=0.105.0,<2" }, + { name = "anthropic", marker = "extra == 'ml'", specifier = ">=0.105.0,<3" }, { name = "asyncio-throttle", marker = "extra == 'youtube'", specifier = ">=1.0.0" }, { name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.29.0" }, { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" },