59b4abaa14
- Default model: anthropic/claude-opus-4-5 → anthropic/claude-opus-4-7 - Quota switcher: claude-opus-4-6 → claude-opus-4-7 - Update all provider defaults and test fixtures - Update comments/docstrings referencing old model names - Claude Opus 4.7 released 2026-04-16, same pricing as 4.6
103 lines
3.3 KiB
Python
103 lines
3.3 KiB
Python
"""Test that the Anthropic OAuth identity block is always included in API requests."""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch, MagicMock
|
|
|
|
import httpx
|
|
|
|
from nanobot.providers.anthropic_oauth import AnthropicOAuthProvider
|
|
from nanobot.providers.oauth_utils import get_claude_code_system_prefix
|
|
|
|
|
|
IDENTITY_TEXT = get_claude_code_system_prefix()
|
|
|
|
|
|
@pytest.fixture
|
|
def provider():
|
|
return AnthropicOAuthProvider(
|
|
oauth_token="sk-ant-oat01-test-token",
|
|
default_model="claude-opus-4-7",
|
|
)
|
|
|
|
|
|
def _mock_response(status_code=200, json_data=None):
|
|
"""Create a mock httpx.Response."""
|
|
resp = MagicMock(spec=httpx.Response)
|
|
resp.status_code = status_code
|
|
resp.headers = {}
|
|
resp.text = ""
|
|
resp.json.return_value = json_data or {
|
|
"content": [{"type": "text", "text": "ok"}],
|
|
"stop_reason": "end_turn",
|
|
"usage": {"input_tokens": 1, "output_tokens": 1},
|
|
}
|
|
return resp
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_identity_block_present_with_system_prompt(provider):
|
|
"""When a system prompt is provided, identity block is the first system block."""
|
|
mock_client = AsyncMock()
|
|
mock_client.post.return_value = _mock_response()
|
|
provider._client = mock_client
|
|
|
|
await provider._make_request(
|
|
messages=[{"role": "user", "content": "hello"}],
|
|
system="You are a helpful assistant.",
|
|
)
|
|
|
|
call_kwargs = mock_client.post.call_args
|
|
payload = call_kwargs.kwargs["json"] if "json" in call_kwargs.kwargs else call_kwargs[1]["json"]
|
|
system_blocks = payload["system"]
|
|
|
|
assert len(system_blocks) == 2
|
|
assert system_blocks[0]["type"] == "text"
|
|
assert system_blocks[0]["text"] == IDENTITY_TEXT
|
|
assert system_blocks[1]["text"] == "You are a helpful assistant."
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_identity_block_present_without_system_prompt(provider):
|
|
"""When no system prompt is provided, identity block is still included.
|
|
|
|
This is the critical fix: extract_facts and similar calls pass system=None,
|
|
but Anthropic requires the identity block for OAuth tokens.
|
|
"""
|
|
mock_client = AsyncMock()
|
|
mock_client.post.return_value = _mock_response()
|
|
provider._client = mock_client
|
|
|
|
await provider._make_request(
|
|
messages=[{"role": "user", "content": "extract facts"}],
|
|
system=None,
|
|
)
|
|
|
|
call_kwargs = mock_client.post.call_args
|
|
payload = call_kwargs.kwargs["json"] if "json" in call_kwargs.kwargs else call_kwargs[1]["json"]
|
|
system_blocks = payload["system"]
|
|
|
|
assert len(system_blocks) == 1
|
|
assert system_blocks[0]["type"] == "text"
|
|
assert system_blocks[0]["text"] == IDENTITY_TEXT
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_identity_block_present_with_empty_string_system(provider):
|
|
"""Empty string system prompt should still include the identity block."""
|
|
mock_client = AsyncMock()
|
|
mock_client.post.return_value = _mock_response()
|
|
provider._client = mock_client
|
|
|
|
await provider._make_request(
|
|
messages=[{"role": "user", "content": "hello"}],
|
|
system="",
|
|
)
|
|
|
|
call_kwargs = mock_client.post.call_args
|
|
payload = call_kwargs.kwargs["json"] if "json" in call_kwargs.kwargs else call_kwargs[1]["json"]
|
|
system_blocks = payload["system"]
|
|
|
|
# Empty string is falsy, so should go through the else branch
|
|
assert len(system_blocks) == 1
|
|
assert system_blocks[0]["text"] == IDENTITY_TEXT
|