{"repo": "agentix-project/agentix", "files": {"plugins/abridge/agentix/bridge/clients/anthropic.py": {"ce174bd053b2d5e58726fc0424e8fa94340ceda7": {"t": 1785674427.8164933, "n": 6956, "h": "ab96bea5fb2e368b", "c": "\"\"\"`AnthropicClient` — pure Anthropic Messages passthrough.\n\nUses the `anthropic` SDK (`anthropic.AsyncAnthropic`) to talk to any\nAnthropic-protocol endpoint (api.anthropic.com or anything that speaks\nthe same shape). Registers two `@on(...)` handlers:\n\n * `/v1/messages` — Messages create\n * `/v1/messages/count_tokens` — token counting\n\nStreaming-vs-not is honoured (`body[\"stream\"]`): for `stream=True` the\nSDK's streaming primitive runs and the chunks are re-serialised as the\nAnthropic SSE wire envelope. For `stream=False`, a single JSON response\ngoes back.\n\n`environ(handle)` returns the env-var bundle (`ANTHROPIC_BASE_URL` +\nplaceholder `ANTHROPIC_API_KEY`) an in-sandbox Anthropic SDK needs to\nroute through the tunnel. Instance method, not module-level — the env\nvars an agent needs are tied to a (client, handle) pair, and methods\nkeep that relationship explicit.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport uuid\nfrom typing import TYPE_CHECKING, Any\n\nfrom agentix.utils import trace\n\nfrom ..proxy import (\n AbridgeError,\n ClientResponse,\n Request,\n TunnelHandle,\n on,\n)\nfrom ._genai_span import populate_anthropic_span\n\nif TYPE_CHECKING:\n from anthropic import AnthropicError, AsyncAnthropic\nelse:\n # `anthropic` is an optional dep: install via `agentix-bridge[anthropic]`.\n try:\n from anthropic import AnthropicError, AsyncAnthropic\n except ImportError:\n AsyncAnthropic = None\n AnthropicError = Exception\n\nlogger = logging.getLogger(__name__)\n\n\n_INSTALL_HINT = (\n \"AnthropicClient requires the anthropic SDK. \"\n \"Install with: pip install 'agentix-bridge[anthropic]'\"\n)\n\n# Anthropic SDKs / Claude Code validate the key's shape locally before\n# any HTTP call — real keys are `sk-ant-api03-<~95 chars>`. Our\n# placeholder matches that prefix so the SDK accepts it; the body of\n# the key spells out that it's not a credential, so leak-detectors and\n# log scrapers don't flag it as a secret. The real upstream key lives\n# only on the host (this client).\nPLACEHOLDER_API_KEY = (\n \"sk-ant-api03-abridge-placeholder-no-real-credentials-leaked-into-sandbox\"\n)\n\n\nclass AnthropicClient:\n \"\"\"Forward Anthropic Messages requests verbatim to a native\n Anthropic endpoint. No shape translation; the upstream IS Anthropic.\n\n `session_id` identifies the rollout this client serves; stamped as\n `x-session-id` on every upstream call (auto-gen if not passed).\n Sharing the same `AnthropicClient` across multiple `Proxy` instances\n means they all map to one session — the intended model.\n\n `model`, if set, overrides the agent's `model` field. Useful for\n pinning many agents to one upstream model without rewriting agents.\n \"\"\"\n\n def __init__(\n self,\n *,\n base_url: str | None = None,\n api_key: str | None = None,\n model: str | None = None,\n timeout: float = 120.0,\n max_retries: int = 0,\n session_id: str | None = None,\n ) -> None:\n if AsyncAnthropic is None:\n raise ImportError(_INSTALL_HINT)\n # 0, not the SDK's 2: silent retries multiply tunnel-window occupancy;\n # retry policy is the operator's call (see Proxy.request_timeout).\n self._client = AsyncAnthropic(\n base_url=base_url, api_key=api_key, timeout=timeout, max_retries=max_retries\n )\n self._model = model\n self.session_id = session_id or uuid.uuid4().hex\n\n @on(\"/v1/messages\")\n async def messages(self, request: Request) -> ClientResponse:\n body = dict(request.body)\n if self._model:\n body[\"model\"] = self._model\n record_id = uuid.uuid4().hex\n extra_headers = {\n \"x-session-id\": self.session_id,\n \"x-request-id\": record_id,\n }\n stream = bool(body.pop(\"stream\", False))\n\n # Open OTel span here (Proxy is trace-blind; caller's contextvar\n # doesn't propagate across HTTP/SIO).\n with trace.span(f\"anthropic messages {body.get('model') or ''}\"):\n try:\n if stream:\n # The SDK exposes a streaming primitive; we drain it\n # and synthesise the original Anthropic SSE blob so\n # the agent's SDK sees a wire stream regardless of\n # how the SDK chose to read events.\n async with self._client.messages.stream(\n **body, extra_headers=extra_headers\n ) as stream_handle:\n async for _ in stream_handle:\n pass\n message = await stream_handle.get_final_message()\n response_dict = message.model_dump(exclude_none=False)\n populate_anthropic_span(request=request.body, response=response_dict)\n return ClientResponse.sse(_anthropic_sse_from_message(response_dict))\n\n message = await self._client.messages.create(\n **body, extra_headers=extra_headers\n )\n except AnthropicError as exc:\n status = int(getattr(exc, \"status_code\", 502) or 502)\n raise AbridgeError(f\"anthropic: {exc}\", status_code=status) from exc\n response_dict = message.model_dump(exclude_none=False)\n populate_anthropic_span(request=request.body, response=response_dict)\n return ClientResponse.json(response_dict)\n\n @on(\"/v1/messages/count_tokens\")\n async def count_tokens(self, request: Request) -> ClientResponse:\n body = dict(request.body)\n if self._model:\n body[\"model\"] = self._model\n try:\n result = await self._client.messages.count_tokens(**body)\n except AnthropicError as exc:\n status = int(getattr(exc, \"status_code\", 502) or 502)\n raise AbridgeError(f\"anthropic: {exc}\", status_code=status) from exc\n return ClientResponse.json(result.model_dump(exclude_none=False))\n\n def environ(self, handle: TunnelHandle) -> dict[str, str]:\n \"\"\"Env-var bundle an in-sandbox Anthropic SDK needs to route\n through `handle`. `ANTHROPIC_API_KEY` is a non-secret placeholder\n whose shape matches Anthropic's real key format so the SDK's\n local validation passes — the real upstream credentials live on\n the host (this client).\"\"\"\n return {\n \"ANTHROPIC_BASE_URL\": handle.url,\n \"ANTHROPIC_API_KEY\": PLACEHOLDER_API_KEY,\n }\n\n\ndef _anthropic_sse_from_message(message: dict[str, Any]) -> bytes:\n \"\"\"Re-serialise a completed Anthropic Message as a single-shot SSE\n blob. Imported lazily to avoid pulling the OpenAI-translation\n helpers when only the pure Anthropic client is used.\"\"\"\n from ._anthropic_transforms import anthropic_sse # noqa: PLC0415\n\n return anthropic_sse(message)\n\n\n__all__ = [\"PLACEHOLDER_API_KEY\", \"AnthropicClient\"]\n"}}}}