194 lines
5.9 KiB
Python
194 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
from openharness.engine.messages import ConversationMessage, ImageBlock, TextBlock
|
|
from openharness.api.client import AnthropicApiClient, OAUTH_BETA_HEADER
|
|
|
|
|
|
def test_anthropic_client_adds_oauth_beta_header(monkeypatch):
|
|
captured: dict[str, object] = {}
|
|
|
|
class _FakeAsyncAnthropic:
|
|
def __init__(self, **kwargs):
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("openharness.api.client.AsyncAnthropic", _FakeAsyncAnthropic)
|
|
|
|
AnthropicApiClient(auth_token="oauth-token")
|
|
|
|
assert captured["auth_token"] == "oauth-token"
|
|
assert captured["default_headers"] == {"anthropic-beta": OAUTH_BETA_HEADER}
|
|
|
|
|
|
def test_anthropic_client_uses_api_key_without_oauth_beta(monkeypatch):
|
|
captured: dict[str, object] = {}
|
|
|
|
class _FakeAsyncAnthropic:
|
|
def __init__(self, **kwargs):
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("openharness.api.client.AsyncAnthropic", _FakeAsyncAnthropic)
|
|
|
|
AnthropicApiClient(api_key="api-key")
|
|
|
|
assert captured["api_key"] == "api-key"
|
|
assert "default_headers" not in captured
|
|
|
|
|
|
def test_anthropic_client_adds_claude_oauth_identity_headers(monkeypatch):
|
|
captured: dict[str, object] = {}
|
|
|
|
class _FakeAsyncAnthropic:
|
|
def __init__(self, **kwargs):
|
|
captured.update(kwargs)
|
|
|
|
monkeypatch.setattr("openharness.api.client.AsyncAnthropic", _FakeAsyncAnthropic)
|
|
monkeypatch.setattr(
|
|
"openharness.auth.external.get_claude_code_version",
|
|
lambda: "2.1.92",
|
|
)
|
|
monkeypatch.setattr(
|
|
"openharness.auth.external.get_claude_code_session_id",
|
|
lambda: "session-123",
|
|
)
|
|
monkeypatch.setattr(
|
|
"openharness.api.client.get_claude_code_session_id",
|
|
lambda: "session-123",
|
|
)
|
|
monkeypatch.setattr(
|
|
"openharness.api.client.claude_attribution_header",
|
|
lambda: "x-anthropic-billing-header: cc_version=2.1.92; cc_entrypoint=cli;",
|
|
)
|
|
|
|
AnthropicApiClient(auth_token="oauth-token", claude_oauth=True)
|
|
|
|
headers = captured["default_headers"]
|
|
assert captured["auth_token"] == "oauth-token"
|
|
assert headers["x-app"] == "cli"
|
|
assert headers["user-agent"] == "claude-cli/2.1.92 (external, cli)"
|
|
assert headers["X-Claude-Code-Session-Id"] == "session-123"
|
|
assert "oauth-2025-04-20" in headers["anthropic-beta"]
|
|
assert "claude-code-20250219" in headers["anthropic-beta"]
|
|
|
|
|
|
def test_conversation_message_serializes_image_block_for_anthropic():
|
|
message = ConversationMessage(
|
|
role="user",
|
|
content=[
|
|
TextBlock(text="Describe this."),
|
|
ImageBlock(media_type="image/png", data="YWJj", source_path="/tmp/example.png"),
|
|
],
|
|
)
|
|
|
|
assert message.to_api_param() == {
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "Describe this."},
|
|
{
|
|
"type": "image",
|
|
"source": {
|
|
"type": "base64",
|
|
"media_type": "image/png",
|
|
"data": "YWJj",
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def test_anthropic_client_refreshes_claude_token_on_request(monkeypatch):
|
|
captured_tokens: list[str] = []
|
|
|
|
class _FakeStream:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
async def __aiter__(self):
|
|
if False:
|
|
yield None
|
|
return
|
|
|
|
async def get_final_message(self):
|
|
class _Usage:
|
|
input_tokens = 1
|
|
output_tokens = 1
|
|
|
|
class _Message:
|
|
usage = _Usage()
|
|
stop_reason = "end_turn"
|
|
role = "assistant"
|
|
content = []
|
|
|
|
return _Message()
|
|
|
|
class _FakeMessages:
|
|
def __init__(self):
|
|
self.last_params = None
|
|
|
|
def stream(self, **params):
|
|
self.last_params = params
|
|
return _FakeStream()
|
|
|
|
class _FakeBeta:
|
|
def __init__(self):
|
|
self.messages = _FakeMessages()
|
|
|
|
class _FakeAsyncAnthropic:
|
|
def __init__(self, **kwargs):
|
|
captured_tokens.append(kwargs["auth_token"])
|
|
self.beta = _FakeBeta()
|
|
self.messages = _FakeMessages()
|
|
|
|
monkeypatch.setattr("openharness.api.client.AsyncAnthropic", _FakeAsyncAnthropic)
|
|
monkeypatch.setattr(
|
|
"openharness.auth.external.get_claude_code_session_id",
|
|
lambda: "session-123",
|
|
)
|
|
monkeypatch.setattr(
|
|
"openharness.api.client.get_claude_code_session_id",
|
|
lambda: "session-123",
|
|
)
|
|
monkeypatch.setattr(
|
|
"openharness.api.client.claude_attribution_header",
|
|
lambda: "x-anthropic-billing-header: cc_version=2.1.92; cc_entrypoint=cli;",
|
|
)
|
|
|
|
current_token = {"value": "initial-token"}
|
|
|
|
client = AnthropicApiClient(
|
|
auth_token="initial-token",
|
|
claude_oauth=True,
|
|
auth_token_resolver=lambda: current_token["value"],
|
|
)
|
|
current_token["value"] = "refreshed-token"
|
|
|
|
from openharness.api.client import ApiMessageRequest
|
|
|
|
async def _run():
|
|
events = []
|
|
async for event in client.stream_message(
|
|
ApiMessageRequest(
|
|
model="claude-sonnet-4-6",
|
|
messages=[],
|
|
system_prompt="system prompt",
|
|
)
|
|
):
|
|
events.append(event)
|
|
return events
|
|
|
|
import asyncio
|
|
|
|
events = asyncio.run(_run())
|
|
|
|
assert captured_tokens == ["initial-token", "refreshed-token"]
|
|
assert events
|
|
assert client._client.beta.messages.last_params["metadata"] == {
|
|
"user_id": '{"device_id":"openharness","session_id":"session-123","account_uuid":""}'
|
|
}
|
|
assert "oauth-2025-04-20" in client._client.beta.messages.last_params["betas"]
|
|
assert client._client.beta.messages.last_params["system"].startswith(
|
|
"x-anthropic-billing-header: cc_version=2.1.92; cc_entrypoint=cli;\n"
|
|
)
|