Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/agentera_agently.json
T

1 line
303 KiB
JSON

{"repo": "agentera/agently", "files": {"examples/agent_task_experiments/_shared.py": {"4aed4449b4c2cae04a14ac6cc30c67d0af0d66b8": {"t": 1785674283.8533616, "n": 11510, "h": "c15a845f8c6c3698", "c": "from __future__ import annotations\n\nimport asyncio\nimport importlib.util\nimport json\nimport os\nimport sys\nimport time\nfrom collections.abc import Mapping\nfrom contextlib import contextmanager\nfrom pathlib import Path\nfrom typing import Any, Iterator, Literal, cast\n\nfrom dotenv import find_dotenv, load_dotenv\n\nREPO_ROOT = Path(__file__).resolve().parents[2]\nif str(REPO_ROOT) not in sys.path:\n sys.path.insert(0, str(REPO_ROOT))\n\nfrom agently import Agently\n\n\nProviderName = Literal[\"longcat\", \"deepseek\", \"ollama\"]\nTASK_MODEL_KEY = \"task-main\"\n\n\ndef _select_provider() -> ProviderName:\n configured = os.getenv(\"AGENT_TASK_EXAMPLE_MODEL_PROVIDER\", \"\").strip().lower()\n if configured in {\"longcat\", \"deepseek\", \"ollama\"}:\n return cast(ProviderName, configured)\n if os.getenv(\"LONGCAT_API_KEY\"):\n return \"longcat\"\n if os.getenv(\"DEEPSEEK_API_KEY\"):\n return \"deepseek\"\n return \"ollama\"\n\n\ndef configure_agent_model_pool(agent: Any, *, temperature: float = 0.0) -> ProviderName:\n load_dotenv(find_dotenv(usecwd=True))\n provider = _select_provider()\n\n model_pool = dict(agent.settings.get(\"model_pool\", {}) or {})\n model_profiles = dict(agent.settings.get(\"model_profiles\", {}) or {})\n api_key_pools = dict(agent.settings.get(\"api_key_pools\", {}) or {})\n\n if provider == \"longcat\":\n api_key = os.getenv(\"LONGCAT_API_KEY\")\n if not api_key:\n raise RuntimeError(\"Missing LONGCAT_API_KEY. Set it or choose another provider.\")\n model_pool[TASK_MODEL_KEY] = \"agent-task-example-longcat\"\n model_profiles[\"agent-task-example-longcat\"] = {\n \"provider\": \"OpenAICompatible\",\n \"base_url\": os.getenv(\"LONGCAT_BASE_URL\", \"https://api.longcat.chat/openai/v1\"),\n \"model\": os.getenv(\"LONGCAT_MODEL\", \"LongCat-Flash-Chat\"),\n \"model_type\": \"chat\",\n \"api_key_pool\": \"agent-task-example-longcat\",\n \"request_options\": {\"temperature\": temperature},\n }\n api_key_pools[\"agent-task-example-longcat\"] = {\n \"selection\": {\"strategy\": \"fixed\"},\n \"keys\": [{\"id\": \"primary\", \"value\": api_key}],\n }\n elif provider == \"deepseek\":\n api_key = os.getenv(\"DEEPSEEK_API_KEY\")\n if not api_key:\n raise RuntimeError(\"Missing DEEPSEEK_API_KEY. Set it or choose another provider.\")\n model_pool[TASK_MODEL_KEY] = \"agent-task-example-deepseek\"\n model_profiles[\"agent-task-example-deepseek\"] = {\n \"provider\": \"OpenAICompatible\",\n \"base_url\": os.getenv(\"DEEPSEEK_BASE_URL\", \"https://api.deepseek.com/v1\"),\n \"model\": os.getenv(\"DEEPSEEK_DEFAULT_MODEL\", \"deepseek-v4-flash\"),\n \"model_type\": \"chat\",\n \"api_key_pool\": \"agent-task-example-deepseek\",\n \"request_options\": {\n \"thinking\": {\"type\": \"disabled\"},\n \"temperature\": temperature,\n },\n }\n api_key_pools[\"agent-task-example-deepseek\"] = {\n \"selection\": {\"strategy\": \"fixed\"},\n \"keys\": [{\"id\": \"primary\", \"value\": api_key}],\n }\n else:\n model_pool[TASK_MODEL_KEY] = \"agent-task-example-ollama\"\n model_profiles[\"agent-task-example-ollama\"] = {\n \"provider\": \"OpenAICompatible\",\n \"base_url\": os.getenv(\"OLLAMA_BASE_URL\", \"http://localhost:11434/v1\"),\n \"model\": os.getenv(\"OLLAMA_DEFAULT_MODEL\", \"qwen2.5:7b\"),\n \"model_type\": \"chat\",\n \"api_key_pool\": \"agent-task-example-ollama\",\n \"request_options\": {\"temperature\": temperature},\n }\n api_key_pools[\"agent-task-example-ollama\"] = {\n \"selection\": {\"strategy\": \"fixed\"},\n \"keys\": [{\"id\": \"local\", \"value\": os.getenv(\"OLLAMA_API_KEY\", \"ollama\")}],\n }\n\n agent.settings.set(\"model_pool\", model_pool)\n agent.settings.set(\"model_profiles\", model_profiles)\n agent.settings.set(\"api_key_pools\", api_key_pools)\n agent.settings.set(\"action.planning_model_key\", TASK_MODEL_KEY)\n agent.settings.set(\n \"skills.runtime.stage_model_keys\",\n {\n \"planner\": TASK_MODEL_KEY,\n \"research\": TASK_MODEL_KEY,\n \"reason\": TASK_MODEL_KEY,\n \"reason_fast\": TASK_MODEL_KEY,\n \"executor\": TASK_MODEL_KEY,\n \"verifier\": TASK_MODEL_KEY,\n \"reflector\": TASK_MODEL_KEY,\n \"finalizer\": TASK_MODEL_KEY,\n },\n )\n agent.activate_model(TASK_MODEL_KEY)\n return provider\n\n\ndef default_workspace(prefix: str) -> Path:\n base = Path(os.getenv(\"AGENT_TASK_EXAMPLE_WORKSPACE\", \".agently/examples/agent_task_experiments\"))\n return (base / f\"{prefix}-{time.strftime('%Y%m%d-%H%M%S')}\").resolve()\n\n\ndef create_task_agent(\n name: str,\n *,\n workspace_prefix: str,\n language: str = \"auto\",\n temperature: float = 0.0,\n) -> tuple[Any, ProviderName, Path]:\n workspace = default_workspace(workspace_prefix)\n workspace.mkdir(parents=True, exist_ok=True)\n agent = Agently.create_agent(name).use_task_workspace(workspace, mode=\"read_write\")\n provider = configure_agent_model_pool(agent, temperature=temperature)\n if language != \"auto\":\n agent.language(language)\n return agent, provider, workspace\n\n\ndef enable_coding_workspace(agent: Any) -> None:\n from agently.builtins.actions import RuntimePreflight\n\n task_workspace = getattr(agent, \"task_workspace\", None)\n if task_workspace is None:\n raise RuntimeError(\"Coding examples require a TaskWorkspace-bound agent.\")\n root = Path(str(task_workspace.root)).resolve()\n RuntimePreflight().register_actions(agent.action, action_id=\"inspect_code_runtimes\")\n agent.enable_task_workspace_file_actions(\n root=root,\n read=True,\n write=True,\n search=True,\n list_files=True,\n coding_agent=True,\n expose_to_model=True,\n )\n agent.enable_shell(\n root=root,\n commands=[\n \"pwd\",\n \"ls\",\n f\"{Path(sys.executable).expanduser().resolve()} reconcile.py\",\n \"python3 reconcile.py\",\n \"python reconcile.py\",\n \"node reconcile.js\",\n \"go run reconcile.go\",\n \"g++ reconcile.cpp\",\n \"clang++ reconcile.cpp\",\n \"./reconcile\",\n ],\n action_id=\"run_bash\",\n desc=(\n \"Only execute or compile the generated reconcile program. Runtime discovery \"\n \"must use inspect_code_runtimes, not shell version commands.\"\n ),\n timeout=60,\n )\n\n\ndef require_mcp_runtime() -> None:\n missing = [\n package\n for package in (\"fastmcp\", \"mcp\")\n if importlib.util.find_spec(package) is None\n ]\n if missing:\n raise RuntimeError(\n \"The mixed MCP examples require optional MCP runtime packages: \"\n f\"{', '.join(missing)}. Install them in the active environment before \"\n \"running examples 06-08; the examples intentionally do not install \"\n \"runtime dependencies for you.\"\n )\n\n\ndef amap_mcp_transport() -> str:\n load_dotenv(find_dotenv(usecwd=True))\n amap_key = os.getenv(\"AMAP_API_KEY\")\n if not amap_key:\n raise RuntimeError(\"AMAP_API_KEY is required for the real AMap MCP travel example.\")\n return f\"https://mcp.amap.com/mcp?key={amap_key}\"\n\n\n@contextmanager\ndef sanitized_proxy_env() -> Iterator[None]:\n names = (\"NO_PROXY\", \"no_proxy\")\n original = {name: os.environ.get(name) for name in names}\n try:\n for name in names:\n value = os.environ.get(name)\n if value:\n os.environ[name] = \",\".join(part for part in value.split(\",\") if part.strip() != \"::1/128\")\n yield\n finally:\n for name, value in original.items():\n if value is None:\n os.environ.pop(name, None)\n else:\n os.environ[name] = value\n\n\ndef install_local_skill(skill_dir: Path, *, registry_root: Path) -> str:\n Agently.skills_executor.configure(\n registry_root=str(registry_root),\n allowed_trust_levels=[\"local\"],\n )\n contract = Agently.skills_executor.install_skills(\n skill_dir,\n trust_level=\"local\",\n update=True,\n )\n skill_id = str(contract.get(\"skill_id\") or \"\").strip()\n if not skill_id:\n raise RuntimeError(f\"Local Skill did not return a skill_id: {skill_dir}\")\n return skill_id\n\n\ndef enable_workspace_report_actions(agent: Any) -> None:\n task_workspace = getattr(agent, \"task_workspace\", None)\n if task_workspace is None:\n raise RuntimeError(\"Report examples require a TaskWorkspace-bound agent.\")\n root = Path(str(task_workspace.root)).resolve()\n agent.enable_task_workspace_file_actions(\n root=root,\n read=True,\n write=True,\n search=True,\n list_files=True,\n expose_to_model=True,\n )\n\n\ndef compact_text(value: Any, *, max_chars: int = 900) -> str:\n text = str(value or \"\").replace(\"\\r\\n\", \"\\n\").replace(\"\\r\", \"\\n\").strip()\n if len(text) <= max_chars:\n return text\n return text[:max_chars].rstrip() + f\" ... [truncated from {len(text)} chars]\"\n\n\ndef stream_options() -> dict[str, Any]:\n return {\"agent_task\": {\"stream_progress\": True}}\n\n\ndef run_and_print(execution: Any, *, provider: ProviderName, task_workspace: Path) -> dict[str, Any]:\n return asyncio.run(\n async_run_and_print(execution, provider=provider, task_workspace=task_workspace)\n )\n\n\nasync def async_run_and_print(\n execution: Any,\n *,\n provider: ProviderName,\n task_workspace: Path,\n) -> dict[str, Any]:\n print(\"[DELTA_STREAM]\")\n delta_text = \"\"\n async for chunk in execution.get_async_generator(type=\"delta\"):\n if not chunk:\n continue\n delta_text += str(chunk)\n print(chunk, end=\"\", flush=True)\n if delta_text and not delta_text.endswith(\"\\n\"):\n print()\n\n result = execution.get_result()\n data = await result.async_get_data()\n full_data = await result.async_get_full_data()\n meta = await result.async_get_meta()\n terminal_data: Mapping[str, Any] = (\n full_data if isinstance(full_data, Mapping) else {}\n )\n nested_result = terminal_data.get(\"result\")\n if not terminal_data.get(\"status\") and isinstance(nested_result, Mapping):\n terminal_data = nested_result\n task_refs = meta.get(\"task_refs\", {}) if isinstance(meta, dict) else {}\n route_logs = meta.get(\"logs\", {}).get(\"route_logs\", {}) if isinstance(meta, dict) else {}\n task_log = route_logs.get(\"agent_task\", {}) if isinstance(route_logs, dict) else {}\n summary = {\n \"provider\": provider,\n \"status\": terminal_data.get(\"status\", \"\"),\n \"accepted\": terminal_data.get(\"accepted\"),\n \"artifact_status\": terminal_data.get(\"artifact_status\"),\n \"missing_criteria\": terminal_data.get(\"missing_criteria\"),\n \"execution_strategy\": (\n terminal_data.get(\"execution_strategy\")\n )\n or (task_refs.get(\"execution_strategy\") if isinstance(task_refs, dict) else None)\n or (task_log.get(\"execution_strategy\") if isinstance(task_log, dict) else None),\n \"task_workspace\": str(task_workspace),\n \"delta_chars\": len(delta_text),\n \"final_preview\": compact_text(data.get(\"final_result\") if isinstance(data, dict) else data),\n }\n print(\"[SUMMARY]\")\n print(json.dumps(summary, ensure_ascii=False, indent=2))\n return summary\n"}}, "examples/execution_resource/_shared.py": {"4aed4449b4c2cae04a14ac6cc30c67d0af0d66b8": {"t": 1785745432.2079, "n": 3235, "h": "0e680b58c16c904b", "c": "from __future__ import annotations\n\nimport os\nfrom pprint import pprint\nfrom typing import Literal\n\nfrom dotenv import find_dotenv, load_dotenv\n\nfrom agently import Agently\nfrom agently.core import Action\n\n\nProviderName = Literal[\"ollama\", \"deepseek\"]\n\n\ndef print_section(title: str, value=None):\n print(f\"\\n[{ title }]\")\n if value is None:\n return\n if isinstance(value, str):\n print(value)\n else:\n pprint(value)\n\n\ndef configure_model(provider: ProviderName, *, temperature: float = 0.0):\n load_dotenv(find_dotenv())\n\n if provider == \"ollama\":\n Agently.set_settings(\n \"OpenAICompatible\",\n {\n \"base_url\": os.getenv(\"OLLAMA_BASE_URL\", \"http://localhost:11434/v1\"),\n \"model\": os.getenv(\"OLLAMA_DEFAULT_MODEL\", \"qwen2.5:7b\"),\n \"request_options\": {\n \"temperature\": temperature,\n },\n },\n )\n elif provider == \"deepseek\":\n api_key = os.getenv(\"DEEPSEEK_API_KEY\")\n if not api_key:\n raise RuntimeError(\"Missing DEEPSEEK_API_KEY. Put it in your environment or .env before running this example.\")\n Agently.set_settings(\n \"OpenAICompatible\",\n {\n \"base_url\": os.getenv(\"DEEPSEEK_BASE_URL\", \"https://api.deepseek.com/v1\"),\n \"model\": os.getenv(\"DEEPSEEK_DEFAULT_MODEL\", \"deepseek-v4-flash\"),\n \"model_type\": \"chat\",\n \"auth\": api_key,\n \"request_options\": {\n \"thinking\": {\"type\": \"disabled\"},\n \"temperature\": temperature,\n },\n },\n )\n else:\n raise ValueError(f\"Unsupported model provider: {provider}\")\n\n Agently.set_settings(\"debug\", False)\n return Agently\n\n\ndef create_agent(provider: ProviderName, system_prompt: str, *, temperature: float = 0.0):\n configure_model(provider, temperature=temperature)\n agent = Agently.create_agent()\n agent.set_agent_prompt(\"system\", system_prompt)\n agent.set_action_loop(max_rounds=4)\n return agent\n\n\ndef print_action_results(records):\n print_section(\"ACTION_RECORDS\", records)\n print_section(\"ACTION_RESULTS_INJECTED_TO_REPLY\", Action.to_action_results(records))\n\n\ndef print_response(result):\n print_section(\"MODEL_REPLY\", result.get_text())\n\n extra = result.full_result_data.get(\"extra\") or {}\n action_logs = extra.get(\"action_logs\", extra.get(\"tool_logs\", [])) if isinstance(extra, dict) else []\n\n print_section(\"ACTION_LOGS_FROM_RESPONSE_RESULT\", action_logs)\n\n# Helper module — no standalone terminal output.\n# Imported by execution_resource examples to provide:\n# create_agent(provider, system_prompt, temperature=0.7) — configures Ollama or DeepSeek\n# print_action_results(records) — prints ACTION_RECORDS and ACTION_RESULTS_INJECTED_TO_REPLY\n# print_response(result) — prints MODEL_REPLY and ACTION_LOGS_FROM_RESPONSE_RESULT\n#\n# How it works:\n# Callers supply provider name (\"ollama\" or \"deepseek\"); create_agent() reads the matching\n# env vars (OLLAMA_BASE_URL or DEEPSEEK_BASE_URL + DEEPSEEK_API_KEY) and configures the\n# OpenAICompatible settings before returning a new agent instance.\n"}, "39e8be8760aa0d9071bc5e5e48ec1a83da4ce1a1": {"t": 1785753139.1565793, "n": 3235, "h": "0e680b58c16c904b", "c": "from __future__ import annotations\n\nimport os\nfrom pprint import pprint\nfrom typing import Literal\n\nfrom dotenv import find_dotenv, load_dotenv\n\nfrom agently import Agently\nfrom agently.core import Action\n\n\nProviderName = Literal[\"ollama\", \"deepseek\"]\n\n\ndef print_section(title: str, value=None):\n print(f\"\\n[{ title }]\")\n if value is None:\n return\n if isinstance(value, str):\n print(value)\n else:\n pprint(value)\n\n\ndef configure_model(provider: ProviderName, *, temperature: float = 0.0):\n load_dotenv(find_dotenv())\n\n if provider == \"ollama\":\n Agently.set_settings(\n \"OpenAICompatible\",\n {\n \"base_url\": os.getenv(\"OLLAMA_BASE_URL\", \"http://localhost:11434/v1\"),\n \"model\": os.getenv(\"OLLAMA_DEFAULT_MODEL\", \"qwen2.5:7b\"),\n \"request_options\": {\n \"temperature\": temperature,\n },\n },\n )\n elif provider == \"deepseek\":\n api_key = os.getenv(\"DEEPSEEK_API_KEY\")\n if not api_key:\n raise RuntimeError(\"Missing DEEPSEEK_API_KEY. Put it in your environment or .env before running this example.\")\n Agently.set_settings(\n \"OpenAICompatible\",\n {\n \"base_url\": os.getenv(\"DEEPSEEK_BASE_URL\", \"https://api.deepseek.com/v1\"),\n \"model\": os.getenv(\"DEEPSEEK_DEFAULT_MODEL\", \"deepseek-v4-flash\"),\n \"model_type\": \"chat\",\n \"auth\": api_key,\n \"request_options\": {\n \"thinking\": {\"type\": \"disabled\"},\n \"temperature\": temperature,\n },\n },\n )\n else:\n raise ValueError(f\"Unsupported model provider: {provider}\")\n\n Agently.set_settings(\"debug\", False)\n return Agently\n\n\ndef create_agent(provider: ProviderName, system_prompt: str, *, temperature: float = 0.0):\n configure_model(provider, temperature=temperature)\n agent = Agently.create_agent()\n agent.set_agent_prompt(\"system\", system_prompt)\n agent.set_action_loop(max_rounds=4)\n return agent\n\n\ndef print_action_results(records):\n print_section(\"ACTION_RECORDS\", records)\n print_section(\"ACTION_RESULTS_INJECTED_TO_REPLY\", Action.to_action_results(records))\n\n\ndef print_response(result):\n print_section(\"MODEL_REPLY\", result.get_text())\n\n extra = result.full_result_data.get(\"extra\") or {}\n action_logs = extra.get(\"action_logs\", extra.get(\"tool_logs\", [])) if isinstance(extra, dict) else []\n\n print_section(\"ACTION_LOGS_FROM_RESPONSE_RESULT\", action_logs)\n\n# Helper module — no standalone terminal output.\n# Imported by execution_resource examples to provide:\n# create_agent(provider, system_prompt, temperature=0.7) — configures Ollama or DeepSeek\n# print_action_results(records) — prints ACTION_RECORDS and ACTION_RESULTS_INJECTED_TO_REPLY\n# print_response(result) — prints MODEL_REPLY and ACTION_LOGS_FROM_RESPONSE_RESULT\n#\n# How it works:\n# Callers supply provider name (\"ollama\" or \"deepseek\"); create_agent() reads the matching\n# env vars (OLLAMA_BASE_URL or DEEPSEEK_BASE_URL + DEEPSEEK_API_KEY) and configures the\n# OpenAICompatible settings before returning a new agent instance.\n"}, "650bd5b516f72ab77805f9531e44d0d09f302a87": {"t": 1785753141.8298593, "n": 3177, "h": "53b2d04118aba0fa", "c": "from __future__ import annotations\n\nimport os\nfrom pprint import pprint\nfrom typing import Literal\n\nfrom dotenv import find_dotenv, load_dotenv\n\nfrom agently import Agently\nfrom agently.core import Action\n\n\nProviderName = Literal[\"ollama\", \"deepseek\"]\n\n\ndef print_section(title: str, value=None):\n print(f\"\\n[{ title }]\")\n if value is None:\n return\n if isinstance(value, str):\n print(value)\n else:\n pprint(value)\n\n\ndef configure_model(provider: ProviderName, *, temperature: float = 0.0):\n load_dotenv(find_dotenv())\n\n if provider == \"ollama\":\n Agently.set_settings(\n \"OpenAICompatible\",\n {\n \"base_url\": os.getenv(\"OLLAMA_BASE_URL\", \"http://localhost:11434/v1\"),\n \"model\": os.getenv(\"OLLAMA_DEFAULT_MODEL\", \"qwen2.5:7b\"),\n \"request_options\": {\n \"temperature\": temperature,\n },\n },\n )\n elif provider == \"deepseek\":\n api_key = os.getenv(\"DEEPSEEK_API_KEY\")\n if not api_key:\n raise RuntimeError(\"Missing DEEPSEEK_API_KEY. Put it in your environment or .env before running this example.\")\n Agently.set_settings(\n \"OpenAICompatible\",\n {\n \"base_url\": os.getenv(\"DEEPSEEK_BASE_URL\", \"https://api.deepseek.com/v1\"),\n \"model\": os.getenv(\"DEEPSEEK_DEFAULT_MODEL\", \"deepseek-chat\"),\n \"model_type\": \"chat\",\n \"auth\": api_key,\n \"request_options\": {\n \"temperature\": temperature,\n },\n },\n )\n else:\n raise ValueError(f\"Unsupported model provider: {provider}\")\n\n Agently.set_settings(\"debug\", False)\n return Agently\n\n\ndef create_agent(provider: ProviderName, system_prompt: str, *, temperature: float = 0.0):\n configure_model(provider, temperature=temperature)\n agent = Agently.create_agent()\n agent.set_agent_prompt(\"system\", system_prompt)\n agent.set_action_loop(max_rounds=4)\n return agent\n\n\ndef print_action_results(records):\n print_section(\"ACTION_RECORDS\", records)\n print_section(\"ACTION_RESULTS_INJECTED_TO_REPLY\", Action.to_action_results(records))\n\n\ndef print_response(result):\n print_section(\"MODEL_REPLY\", result.get_text())\n\n extra = result.full_result_data.get(\"extra\") or {}\n action_logs = extra.get(\"action_logs\", extra.get(\"tool_logs\", [])) if isinstance(extra, dict) else []\n\n print_section(\"ACTION_LOGS_FROM_RESPONSE_RESULT\", action_logs)\n\n# Helper module — no standalone terminal output.\n# Imported by execution_resource examples to provide:\n# create_agent(provider, system_prompt, temperature=0.7) — configures Ollama or DeepSeek\n# print_action_results(records) — prints ACTION_RECORDS and ACTION_RESULTS_INJECTED_TO_REPLY\n# print_response(result) — prints MODEL_REPLY and ACTION_LOGS_FROM_RESPONSE_RESULT\n#\n# How it works:\n# Callers supply provider name (\"ollama\" or \"deepseek\"); create_agent() reads the matching\n# env vars (OLLAMA_BASE_URL or DEEPSEEK_BASE_URL + DEEPSEEK_API_KEY) and configures the\n# OpenAICompatible settings before returning a new agent instance.\n"}}, "tests/test_plugins/test_model_requester/test_anthropic_compatible.py": {"4aed4449b4c2cae04a14ac6cc30c67d0af0d66b8": {"t": 1785746636.978035, "n": 29607, "h": "d802227ca1a3746d", "c": "import asyncio\nimport json\nfrom types import SimpleNamespace\nfrom typing import Any\n\nimport pytest\nfrom httpx import ReadError\nfrom httpx_sse import SSEError\n\nfrom agently import Agently\nfrom agently.core.application.AgentExecution import RuntimeStageStallError\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\n@pytest.mark.asyncio\nasync def test_request_retry_uses_public_attempt_boundary(monkeypatch):\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"request_retry\": {\"max_attempts\": 2, \"after_output\": True},\n },\n {\"input\": \"hello\"},\n )\n calls = 0\n\n async def fake_legacy(_request_data):\n nonlocal calls\n calls += 1\n if calls == 1:\n yield \"content_block_delta\", '{\"delta\":{\"text\":\"partial\"}}'\n yield \"error\", ReadError(\"anthropic SSE disconnected\")\n return\n yield \"message_stop\", '{\"type\":\"message_stop\"}'\n\n monkeypatch.setattr(plugin, \"_request_model_legacy\", fake_legacy)\n events = [\n item\n async for item in plugin.request_model(plugin.generate_request_data())\n ]\n\n assert calls == 2\n assert events[1] == (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"anthropic SSE disconnected\",\n \"error_type\": \"ReadError\",\n },\n )\n assert events[-1] == (\n \"status\",\n {\"status\": \"completed\", \"attempt_index\": 2, \"retry\": False},\n )\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"stream\", [False, True])\nasync def test_http_error_message_excludes_request_body(monkeypatch: pytest.MonkeyPatch, stream: bool):\n class FakeResponse:\n status_code = 500\n content = b'{\"error\":{\"message\":\"upstream failed\"}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n return FakeResponse()\n\n async def fail_sse(*args, **kwargs):\n raise SSEError(\"provider returned non-SSE response\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n if stream:\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fail_sse)\n request_marker = f\"private-anthropic-{'stream-' if stream else ''}request-marker\"\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-test\",\n \"stream\": stream,\n \"request_retry\": {\"max_attempts\": 1},\n },\n {\"input\": request_marker},\n )\n\n events = [item async for item in plugin.request_model(plugin.generate_request_data())]\n error = next(payload for event, payload in events if event == \"error\")\n message = str(error)\n\n assert \"Status Code: 500\" in message\n assert \"upstream failed\" in message\n assert \"Request Data:\" not in message\n assert request_marker not in message\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, Any]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_generate_request_preserves_thinking_options():\n request = generate_request(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"request_options\": {\n \"thinking\": {\"type\": \"enabled\"},\n \"output_config\": {\"effort\": \"high\"},\n },\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.deepseek.com/anthropic/messages\"\n assert request[\"request_options\"][\"thinking\"] == {\"type\": \"enabled\"}\n assert request[\"request_options\"][\"output_config\"] == {\"effort\": \"high\"}\n\n\ndef test_generate_request_replays_thinking_signature_and_tool_result():\n request = generate_request(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n },\n {\n \"chat_history\": [\n {\"role\": \"user\", \"content\": \"What is the date?\"},\n {\n \"role\": \"assistant\",\n \"content\": [\n {\"type\": \"thinking\", \"thinking\": \"Use the date tool.\", \"signature\": \"signed-thinking\"},\n {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"get_date\", \"input\": {}},\n ],\n },\n ],\n \"attachment\": [\n {\"type\": \"tool_result\", \"tool_use_id\": \"toolu_1\", \"content\": \"2026-07-31\"},\n ],\n },\n )\n\n messages = request[\"data\"][\"messages\"]\n assert messages[1][\"content\"] == [\n {\"type\": \"thinking\", \"thinking\": \"Use the date tool.\", \"signature\": \"signed-thinking\"},\n {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"get_date\", \"input\": {}},\n ]\n assert messages[2][\"content\"] == [\n {\"type\": \"tool_result\", \"tool_use_id\": \"toolu_1\", \"content\": \"2026-07-31\"},\n ]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n \"required_input_keys\": [\"issue_id\"],\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"required\"] == [\"issue_id\"]\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_idle_timeout_returns_timeout_error_event(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hello\"}}',\n )\n await asyncio.sleep(0.05)\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": True,\n \"stream_idle_timeout\": 0.01,\n \"timeout\": {\"connect\": 1.0, \"read\": 9.0, \"write\": 2.0, \"pool\": 3.0},\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert len(events) == 3\n assert events[0][0] == \"content_block_delta\"\n assert events[1][0] == \"status\"\n assert events[1][1][\"status\"] == \"failed\"\n assert events[1][1][\"retry\"] is False\n assert events[1][1][\"reason\"] == \"Stream idle timeout after 0.01 seconds.\"\n assert events[2][0] == \"error\"\n assert isinstance(events[2][1], RuntimeStageStallError)\n assert events[2][1].stage == \"response_stream\"\n\n\n@pytest.mark.asyncio\nasync def test_streaming_stops_transport_after_message_stop(monkeypatch: pytest.MonkeyPatch):\n late_event_reached = False\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n nonlocal late_event_reached\n yield SimpleNamespace(event=\"message_stop\", data='{\"type\":\"message_stop\"}')\n late_event_reached = True\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"stream\": True,\n },\n {\"input\": \"hello\"},\n )\n\n events = [item async for item in plugin.request_model(plugin.generate_request_data())]\n\n assert late_event_reached is False\n assert [event for event, _payload in events] == [\"message_stop\", \"status\"]\n assert events[-1][1][\"status\"] == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_returns_stall_error(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json, headers\n await asyncio.sleep(10)\n raise AssertionError(\"post should have been cancelled by the idle deadline\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n statuses = [payload for event, payload in events if event == \"status\"]\n assert statuses\n assert statuses[-1][\"status\"] == \"failed\"\n assert any(\n \"Non-streaming response made no progress before idle deadline\" in str(payload.get(\"reason\") or \"\")\n for payload in statuses\n )\n assert events[-1][0] == \"error\"\n assert isinstance(events[-1][1], RuntimeStageStallError)\n assert events[-1][1].stage == \"response_materialization\"\n assert events[-1][1].timeout_seconds == 0.01\n assert \"stream_idle_timeout=0.01\" in str(events[-1][1])\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_allows_api_key_failover(monkeypatch: pytest.MonkeyPatch):\n calls: list[dict[str, str]] = []\n\n class FakeResponse:\n def __init__(self, status_code: int, content: bytes):\n self.status_code = status_code\n self.content = content\n self.text = content.decode()\n self.headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json\n calls.append(dict(self.headers if headers is None else headers))\n if len(calls) == 1:\n await asyncio.sleep(10)\n raise AssertionError(\"first post should have been cancelled by the idle deadline\")\n return FakeResponse(\n 200,\n b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n )\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"api_key\": \"key-a\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n \"_api_key_pool_runtime\": {\n \"pool_id\": \"example\",\n \"failover\": {\"handler\": lambda _context: \"try_next\", \"max_attempts\": 2},\n \"keys\": [\n {\"id\": \"a\", \"value\": \"key-a\", \"index\": 0},\n {\"id\": \"b\", \"value\": \"key-b\", \"index\": 1},\n ],\n \"selected_key_id\": \"a\",\n \"attempts\": [{\"key_id\": \"a\", \"action\": \"initial\"}],\n },\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert [headers.get(\"x-api-key\") for headers in calls] == [\"key-a\", \"key-b\"]\n assert events == [\n (\n \"message\",\n '{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ]\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n assert (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}) in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_non_stream_thinking_blocks():\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"stream\": False,\n },\n {\"input\": \"hello\"},\n )\n events = collect_events(\n plugin,\n [\n (\n \"message\",\n json.dumps(\n {\n \"id\": \"msg_ds\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"deepseek-v4-flash\",\n \"content\": [\n {\"type\": \"thinking\", \"thinking\": \"model reasoning\", \"signature\": \"signed-thinking\"},\n {\"type\": \"text\", \"text\": \"done text\"},\n ],\n \"stop_reason\": \"end_turn\",\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ],\n )\n\n assert (\"reasoning_done\", \"model reasoning\") in events\n assert (\"done\", \"done text\") in events\n original_done = next(payload for event, payload in events if event == \"original_done\")\n assert original_done[\"content\"][0] == {\n \"type\": \"thinking\",\n \"thinking\": \"model reasoning\",\n \"signature\": \"signed-thinking\",\n }\n assert (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}) in events\n\n\ndef test_broadcast_response_preserves_core_status_record():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})],\n )\n\n assert events == [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})]\n\n\ndef test_broadcast_response_resets_attempt_text_after_retry_status():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"partial\"}}),\n ),\n (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"server disconnected\",\n },\n ),\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"replacement\"}}),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert [payload for event, payload in events if event == \"delta\"] == [\"partial\", \"replacement\"]\n assert (\"done\", \"replacement\") in events\n assert (\"done\", \"partialreplacement\") not in events\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "ddef3be83bc7c7803741124118e27fc2bcdba9d0": {"t": 1785753139.3980548, "n": 29607, "h": "d802227ca1a3746d", "c": "import asyncio\nimport json\nfrom types import SimpleNamespace\nfrom typing import Any\n\nimport pytest\nfrom httpx import ReadError\nfrom httpx_sse import SSEError\n\nfrom agently import Agently\nfrom agently.core.application.AgentExecution import RuntimeStageStallError\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\n@pytest.mark.asyncio\nasync def test_request_retry_uses_public_attempt_boundary(monkeypatch):\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"request_retry\": {\"max_attempts\": 2, \"after_output\": True},\n },\n {\"input\": \"hello\"},\n )\n calls = 0\n\n async def fake_legacy(_request_data):\n nonlocal calls\n calls += 1\n if calls == 1:\n yield \"content_block_delta\", '{\"delta\":{\"text\":\"partial\"}}'\n yield \"error\", ReadError(\"anthropic SSE disconnected\")\n return\n yield \"message_stop\", '{\"type\":\"message_stop\"}'\n\n monkeypatch.setattr(plugin, \"_request_model_legacy\", fake_legacy)\n events = [\n item\n async for item in plugin.request_model(plugin.generate_request_data())\n ]\n\n assert calls == 2\n assert events[1] == (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"anthropic SSE disconnected\",\n \"error_type\": \"ReadError\",\n },\n )\n assert events[-1] == (\n \"status\",\n {\"status\": \"completed\", \"attempt_index\": 2, \"retry\": False},\n )\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"stream\", [False, True])\nasync def test_http_error_message_excludes_request_body(monkeypatch: pytest.MonkeyPatch, stream: bool):\n class FakeResponse:\n status_code = 500\n content = b'{\"error\":{\"message\":\"upstream failed\"}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n return FakeResponse()\n\n async def fail_sse(*args, **kwargs):\n raise SSEError(\"provider returned non-SSE response\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n if stream:\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fail_sse)\n request_marker = f\"private-anthropic-{'stream-' if stream else ''}request-marker\"\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-test\",\n \"stream\": stream,\n \"request_retry\": {\"max_attempts\": 1},\n },\n {\"input\": request_marker},\n )\n\n events = [item async for item in plugin.request_model(plugin.generate_request_data())]\n error = next(payload for event, payload in events if event == \"error\")\n message = str(error)\n\n assert \"Status Code: 500\" in message\n assert \"upstream failed\" in message\n assert \"Request Data:\" not in message\n assert request_marker not in message\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, Any]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_generate_request_preserves_thinking_options():\n request = generate_request(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"request_options\": {\n \"thinking\": {\"type\": \"enabled\"},\n \"output_config\": {\"effort\": \"high\"},\n },\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.deepseek.com/anthropic/messages\"\n assert request[\"request_options\"][\"thinking\"] == {\"type\": \"enabled\"}\n assert request[\"request_options\"][\"output_config\"] == {\"effort\": \"high\"}\n\n\ndef test_generate_request_replays_thinking_signature_and_tool_result():\n request = generate_request(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n },\n {\n \"chat_history\": [\n {\"role\": \"user\", \"content\": \"What is the date?\"},\n {\n \"role\": \"assistant\",\n \"content\": [\n {\"type\": \"thinking\", \"thinking\": \"Use the date tool.\", \"signature\": \"signed-thinking\"},\n {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"get_date\", \"input\": {}},\n ],\n },\n ],\n \"attachment\": [\n {\"type\": \"tool_result\", \"tool_use_id\": \"toolu_1\", \"content\": \"2026-07-31\"},\n ],\n },\n )\n\n messages = request[\"data\"][\"messages\"]\n assert messages[1][\"content\"] == [\n {\"type\": \"thinking\", \"thinking\": \"Use the date tool.\", \"signature\": \"signed-thinking\"},\n {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"get_date\", \"input\": {}},\n ]\n assert messages[2][\"content\"] == [\n {\"type\": \"tool_result\", \"tool_use_id\": \"toolu_1\", \"content\": \"2026-07-31\"},\n ]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n \"required_input_keys\": [\"issue_id\"],\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"required\"] == [\"issue_id\"]\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_idle_timeout_returns_timeout_error_event(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hello\"}}',\n )\n await asyncio.sleep(0.05)\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": True,\n \"stream_idle_timeout\": 0.01,\n \"timeout\": {\"connect\": 1.0, \"read\": 9.0, \"write\": 2.0, \"pool\": 3.0},\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert len(events) == 3\n assert events[0][0] == \"content_block_delta\"\n assert events[1][0] == \"status\"\n assert events[1][1][\"status\"] == \"failed\"\n assert events[1][1][\"retry\"] is False\n assert events[1][1][\"reason\"] == \"Stream idle timeout after 0.01 seconds.\"\n assert events[2][0] == \"error\"\n assert isinstance(events[2][1], RuntimeStageStallError)\n assert events[2][1].stage == \"response_stream\"\n\n\n@pytest.mark.asyncio\nasync def test_streaming_stops_transport_after_message_stop(monkeypatch: pytest.MonkeyPatch):\n late_event_reached = False\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n nonlocal late_event_reached\n yield SimpleNamespace(event=\"message_stop\", data='{\"type\":\"message_stop\"}')\n late_event_reached = True\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"stream\": True,\n },\n {\"input\": \"hello\"},\n )\n\n events = [item async for item in plugin.request_model(plugin.generate_request_data())]\n\n assert late_event_reached is False\n assert [event for event, _payload in events] == [\"message_stop\", \"status\"]\n assert events[-1][1][\"status\"] == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_returns_stall_error(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json, headers\n await asyncio.sleep(10)\n raise AssertionError(\"post should have been cancelled by the idle deadline\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n statuses = [payload for event, payload in events if event == \"status\"]\n assert statuses\n assert statuses[-1][\"status\"] == \"failed\"\n assert any(\n \"Non-streaming response made no progress before idle deadline\" in str(payload.get(\"reason\") or \"\")\n for payload in statuses\n )\n assert events[-1][0] == \"error\"\n assert isinstance(events[-1][1], RuntimeStageStallError)\n assert events[-1][1].stage == \"response_materialization\"\n assert events[-1][1].timeout_seconds == 0.01\n assert \"stream_idle_timeout=0.01\" in str(events[-1][1])\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_allows_api_key_failover(monkeypatch: pytest.MonkeyPatch):\n calls: list[dict[str, str]] = []\n\n class FakeResponse:\n def __init__(self, status_code: int, content: bytes):\n self.status_code = status_code\n self.content = content\n self.text = content.decode()\n self.headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json\n calls.append(dict(self.headers if headers is None else headers))\n if len(calls) == 1:\n await asyncio.sleep(10)\n raise AssertionError(\"first post should have been cancelled by the idle deadline\")\n return FakeResponse(\n 200,\n b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n )\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"api_key\": \"key-a\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n \"_api_key_pool_runtime\": {\n \"pool_id\": \"example\",\n \"failover\": {\"handler\": lambda _context: \"try_next\", \"max_attempts\": 2},\n \"keys\": [\n {\"id\": \"a\", \"value\": \"key-a\", \"index\": 0},\n {\"id\": \"b\", \"value\": \"key-b\", \"index\": 1},\n ],\n \"selected_key_id\": \"a\",\n \"attempts\": [{\"key_id\": \"a\", \"action\": \"initial\"}],\n },\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert [headers.get(\"x-api-key\") for headers in calls] == [\"key-a\", \"key-b\"]\n assert events == [\n (\n \"message\",\n '{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ]\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n assert (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}) in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_non_stream_thinking_blocks():\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"stream\": False,\n },\n {\"input\": \"hello\"},\n )\n events = collect_events(\n plugin,\n [\n (\n \"message\",\n json.dumps(\n {\n \"id\": \"msg_ds\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"deepseek-v4-flash\",\n \"content\": [\n {\"type\": \"thinking\", \"thinking\": \"model reasoning\", \"signature\": \"signed-thinking\"},\n {\"type\": \"text\", \"text\": \"done text\"},\n ],\n \"stop_reason\": \"end_turn\",\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ],\n )\n\n assert (\"reasoning_done\", \"model reasoning\") in events\n assert (\"done\", \"done text\") in events\n original_done = next(payload for event, payload in events if event == \"original_done\")\n assert original_done[\"content\"][0] == {\n \"type\": \"thinking\",\n \"thinking\": \"model reasoning\",\n \"signature\": \"signed-thinking\",\n }\n assert (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}) in events\n\n\ndef test_broadcast_response_preserves_core_status_record():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})],\n )\n\n assert events == [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})]\n\n\ndef test_broadcast_response_resets_attempt_text_after_retry_status():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"partial\"}}),\n ),\n (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"server disconnected\",\n },\n ),\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"replacement\"}}),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert [payload for event, payload in events if event == \"delta\"] == [\"partial\", \"replacement\"]\n assert (\"done\", \"replacement\") in events\n assert (\"done\", \"partialreplacement\") not in events\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "0e3b6f874490f57bcc143b42046aa9ebff7de826": {"t": 1785753144.2591457, "n": 28273, "h": "cd437efedc0cfae8", "c": "import asyncio\nimport json\nfrom types import SimpleNamespace\nfrom typing import Any\n\nimport pytest\nfrom httpx_sse import SSEError\n\nfrom agently import Agently\nfrom agently.core.application.AgentExecution import RuntimeStageStallError\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"stream\", [False, True])\nasync def test_http_error_message_excludes_request_body(monkeypatch: pytest.MonkeyPatch, stream: bool):\n class FakeResponse:\n status_code = 500\n content = b'{\"error\":{\"message\":\"upstream failed\"}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n return FakeResponse()\n\n async def fail_sse(*args, **kwargs):\n raise SSEError(\"provider returned non-SSE response\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n if stream:\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fail_sse)\n request_marker = f\"private-anthropic-{'stream-' if stream else ''}request-marker\"\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-test\",\n \"stream\": stream,\n \"request_retry\": {\"max_attempts\": 1},\n },\n {\"input\": request_marker},\n )\n\n events = [item async for item in plugin.request_model(plugin.generate_request_data())]\n error = next(payload for event, payload in events if event == \"error\")\n message = str(error)\n\n assert \"Status Code: 500\" in message\n assert \"upstream failed\" in message\n assert \"Request Data:\" not in message\n assert request_marker not in message\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, Any]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_generate_request_preserves_thinking_options():\n request = generate_request(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"request_options\": {\n \"thinking\": {\"type\": \"enabled\"},\n \"output_config\": {\"effort\": \"high\"},\n },\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.deepseek.com/anthropic/messages\"\n assert request[\"request_options\"][\"thinking\"] == {\"type\": \"enabled\"}\n assert request[\"request_options\"][\"output_config\"] == {\"effort\": \"high\"}\n\n\ndef test_generate_request_replays_thinking_signature_and_tool_result():\n request = generate_request(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n },\n {\n \"chat_history\": [\n {\"role\": \"user\", \"content\": \"What is the date?\"},\n {\n \"role\": \"assistant\",\n \"content\": [\n {\"type\": \"thinking\", \"thinking\": \"Use the date tool.\", \"signature\": \"signed-thinking\"},\n {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"get_date\", \"input\": {}},\n ],\n },\n ],\n \"attachment\": [\n {\"type\": \"tool_result\", \"tool_use_id\": \"toolu_1\", \"content\": \"2026-07-31\"},\n ],\n },\n )\n\n messages = request[\"data\"][\"messages\"]\n assert messages[1][\"content\"] == [\n {\"type\": \"thinking\", \"thinking\": \"Use the date tool.\", \"signature\": \"signed-thinking\"},\n {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"get_date\", \"input\": {}},\n ]\n assert messages[2][\"content\"] == [\n {\"type\": \"tool_result\", \"tool_use_id\": \"toolu_1\", \"content\": \"2026-07-31\"},\n ]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n \"required_input_keys\": [\"issue_id\"],\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"required\"] == [\"issue_id\"]\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_idle_timeout_returns_timeout_error_event(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hello\"}}',\n )\n await asyncio.sleep(0.05)\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": True,\n \"stream_idle_timeout\": 0.01,\n \"timeout\": {\"connect\": 1.0, \"read\": 9.0, \"write\": 2.0, \"pool\": 3.0},\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert len(events) == 3\n assert events[0][0] == \"content_block_delta\"\n assert events[1][0] == \"status\"\n assert events[1][1][\"status\"] == \"failed\"\n assert events[1][1][\"retry\"] is False\n assert events[1][1][\"reason\"] == \"Stream idle timeout after 0.01 seconds.\"\n assert events[2][0] == \"error\"\n assert isinstance(events[2][1], RuntimeStageStallError)\n assert events[2][1].stage == \"response_stream\"\n\n\n@pytest.mark.asyncio\nasync def test_streaming_stops_transport_after_message_stop(monkeypatch: pytest.MonkeyPatch):\n late_event_reached = False\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n nonlocal late_event_reached\n yield SimpleNamespace(event=\"message_stop\", data='{\"type\":\"message_stop\"}')\n late_event_reached = True\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"stream\": True,\n },\n {\"input\": \"hello\"},\n )\n\n events = [item async for item in plugin.request_model(plugin.generate_request_data())]\n\n assert late_event_reached is False\n assert [event for event, _payload in events] == [\"message_stop\", \"status\"]\n assert events[-1][1][\"status\"] == \"completed\"\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_returns_stall_error(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json, headers\n await asyncio.sleep(10)\n raise AssertionError(\"post should have been cancelled by the idle deadline\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n statuses = [payload for event, payload in events if event == \"status\"]\n assert statuses\n assert statuses[-1][\"status\"] == \"failed\"\n assert any(\n \"Non-streaming response made no progress before idle deadline\" in str(payload.get(\"reason\") or \"\")\n for payload in statuses\n )\n assert events[-1][0] == \"error\"\n assert isinstance(events[-1][1], RuntimeStageStallError)\n assert events[-1][1].stage == \"response_materialization\"\n assert events[-1][1].timeout_seconds == 0.01\n assert \"stream_idle_timeout=0.01\" in str(events[-1][1])\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_allows_api_key_failover(monkeypatch: pytest.MonkeyPatch):\n calls: list[dict[str, str]] = []\n\n class FakeResponse:\n def __init__(self, status_code: int, content: bytes):\n self.status_code = status_code\n self.content = content\n self.text = content.decode()\n self.headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json\n calls.append(dict(self.headers if headers is None else headers))\n if len(calls) == 1:\n await asyncio.sleep(10)\n raise AssertionError(\"first post should have been cancelled by the idle deadline\")\n return FakeResponse(\n 200,\n b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n )\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"api_key\": \"key-a\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n \"_api_key_pool_runtime\": {\n \"pool_id\": \"example\",\n \"failover\": {\"handler\": lambda _context: \"try_next\", \"max_attempts\": 2},\n \"keys\": [\n {\"id\": \"a\", \"value\": \"key-a\", \"index\": 0},\n {\"id\": \"b\", \"value\": \"key-b\", \"index\": 1},\n ],\n \"selected_key_id\": \"a\",\n \"attempts\": [{\"key_id\": \"a\", \"action\": \"initial\"}],\n },\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert [headers.get(\"x-api-key\") for headers in calls] == [\"key-a\", \"key-b\"]\n assert events == [\n (\n \"message\",\n '{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ]\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n assert (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}) in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_non_stream_thinking_blocks():\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.deepseek.com/anthropic\",\n \"model\": \"deepseek-v4-flash\",\n \"stream\": False,\n },\n {\"input\": \"hello\"},\n )\n events = collect_events(\n plugin,\n [\n (\n \"message\",\n json.dumps(\n {\n \"id\": \"msg_ds\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"deepseek-v4-flash\",\n \"content\": [\n {\"type\": \"thinking\", \"thinking\": \"model reasoning\", \"signature\": \"signed-thinking\"},\n {\"type\": \"text\", \"text\": \"done text\"},\n ],\n \"stop_reason\": \"end_turn\",\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ],\n )\n\n assert (\"reasoning_done\", \"model reasoning\") in events\n assert (\"done\", \"done text\") in events\n original_done = next(payload for event, payload in events if event == \"original_done\")\n assert original_done[\"content\"][0] == {\n \"type\": \"thinking\",\n \"thinking\": \"model reasoning\",\n \"signature\": \"signed-thinking\",\n }\n assert (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}) in events\n\n\ndef test_broadcast_response_preserves_core_status_record():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})],\n )\n\n assert events == [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})]\n\n\ndef test_broadcast_response_resets_attempt_text_after_retry_status():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"partial\"}}),\n ),\n (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"server disconnected\",\n },\n ),\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"replacement\"}}),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert [payload for event, payload in events if event == \"delta\"] == [\"partial\", \"replacement\"]\n assert (\"done\", \"replacement\") in events\n assert (\"done\", \"partialreplacement\") not in events\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "0bafc89e46fe38a305136a1b771e874dadbd2c43": {"t": 1785753146.1877193, "n": 22984, "h": "1b1c2d448aaaa6fa", "c": "import asyncio\nimport json\nfrom types import SimpleNamespace\nfrom typing import Any\n\nimport pytest\nfrom httpx_sse import SSEError\n\nfrom agently import Agently\nfrom agently.core.application.AgentExecution import RuntimeStageStallError\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"stream\", [False, True])\nasync def test_http_error_message_excludes_request_body(monkeypatch: pytest.MonkeyPatch, stream: bool):\n class FakeResponse:\n status_code = 500\n content = b'{\"error\":{\"message\":\"upstream failed\"}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n return FakeResponse()\n\n async def fail_sse(*args, **kwargs):\n raise SSEError(\"provider returned non-SSE response\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n if stream:\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fail_sse)\n request_marker = f\"private-anthropic-{'stream-' if stream else ''}request-marker\"\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-test\",\n \"stream\": stream,\n \"request_retry\": {\"max_attempts\": 1},\n },\n {\"input\": request_marker},\n )\n\n events = [item async for item in plugin.request_model(plugin.generate_request_data())]\n error = next(payload for event, payload in events if event == \"error\")\n message = str(error)\n\n assert \"Status Code: 500\" in message\n assert \"upstream failed\" in message\n assert \"Request Data:\" not in message\n assert request_marker not in message\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, Any]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n \"required_input_keys\": [\"issue_id\"],\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"required\"] == [\"issue_id\"]\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_idle_timeout_returns_timeout_error_event(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hello\"}}',\n )\n await asyncio.sleep(0.05)\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": True,\n \"stream_idle_timeout\": 0.01,\n \"timeout\": {\"connect\": 1.0, \"read\": 9.0, \"write\": 2.0, \"pool\": 3.0},\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert len(events) == 3\n assert events[0][0] == \"content_block_delta\"\n assert events[1][0] == \"status\"\n assert events[1][1][\"status\"] == \"failed\"\n assert events[1][1][\"retry\"] is False\n assert events[1][1][\"reason\"] == \"Stream idle timeout after 0.01 seconds.\"\n assert events[2][0] == \"error\"\n assert isinstance(events[2][1], RuntimeStageStallError)\n assert events[2][1].stage == \"response_stream\"\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_returns_stall_error(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json, headers\n await asyncio.sleep(10)\n raise AssertionError(\"post should have been cancelled by the idle deadline\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n statuses = [payload for event, payload in events if event == \"status\"]\n assert statuses\n assert statuses[-1][\"status\"] == \"failed\"\n assert any(\n \"Non-streaming response made no progress before idle deadline\" in str(payload.get(\"reason\") or \"\")\n for payload in statuses\n )\n assert events[-1][0] == \"error\"\n assert isinstance(events[-1][1], RuntimeStageStallError)\n assert events[-1][1].stage == \"response_materialization\"\n assert events[-1][1].timeout_seconds == 0.01\n assert \"stream_idle_timeout=0.01\" in str(events[-1][1])\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_allows_api_key_failover(monkeypatch: pytest.MonkeyPatch):\n calls: list[dict[str, str]] = []\n\n class FakeResponse:\n def __init__(self, status_code: int, content: bytes):\n self.status_code = status_code\n self.content = content\n self.text = content.decode()\n self.headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json\n calls.append(dict(self.headers if headers is None else headers))\n if len(calls) == 1:\n await asyncio.sleep(10)\n raise AssertionError(\"first post should have been cancelled by the idle deadline\")\n return FakeResponse(\n 200,\n b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n )\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"api_key\": \"key-a\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n \"_api_key_pool_runtime\": {\n \"pool_id\": \"example\",\n \"failover\": {\"handler\": lambda _context: \"try_next\", \"max_attempts\": 2},\n \"keys\": [\n {\"id\": \"a\", \"value\": \"key-a\", \"index\": 0},\n {\"id\": \"b\", \"value\": \"key-b\", \"index\": 1},\n ],\n \"selected_key_id\": \"a\",\n \"attempts\": [{\"key_id\": \"a\", \"action\": \"initial\"}],\n },\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert [headers.get(\"x-api-key\") for headers in calls] == [\"key-a\", \"key-b\"]\n assert events == [\n (\n \"message\",\n '{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ]\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_preserves_core_status_record():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})],\n )\n\n assert events == [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})]\n\n\ndef test_broadcast_response_resets_attempt_text_after_retry_status():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"partial\"}}),\n ),\n (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"server disconnected\",\n },\n ),\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"replacement\"}}),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert [payload for event, payload in events if event == \"delta\"] == [\"partial\", \"replacement\"]\n assert (\"done\", \"replacement\") in events\n assert (\"done\", \"partialreplacement\") not in events\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "c5581dc567b11c5235dcaa5dc1cd1d3c0d818f4c": {"t": 1785753147.5101478, "n": 22859, "h": "120784d16e030c57", "c": "import asyncio\nimport json\nfrom types import SimpleNamespace\nfrom typing import Any\n\nimport pytest\nfrom httpx_sse import SSEError\n\nfrom agently import Agently\nfrom agently.core.application.AgentExecution import RuntimeStageStallError\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\n@pytest.mark.asyncio\n@pytest.mark.parametrize(\"stream\", [False, True])\nasync def test_http_error_message_excludes_request_body(monkeypatch: pytest.MonkeyPatch, stream: bool):\n class FakeResponse:\n status_code = 500\n content = b'{\"error\":{\"message\":\"upstream failed\"}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n return FakeResponse()\n\n async def fail_sse(*args, **kwargs):\n raise SSEError(\"provider returned non-SSE response\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n if stream:\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fail_sse)\n request_marker = f\"private-anthropic-{'stream-' if stream else ''}request-marker\"\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-test\",\n \"stream\": stream,\n \"request_retry\": {\"max_attempts\": 1},\n },\n {\"input\": request_marker},\n )\n\n events = [item async for item in plugin.request_model(plugin.generate_request_data())]\n error = next(payload for event, payload in events if event == \"error\")\n message = str(error)\n\n assert \"Status Code: 500\" in message\n assert \"upstream failed\" in message\n assert \"Request Data:\" not in message\n assert request_marker not in message\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, Any]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_idle_timeout_returns_timeout_error_event(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hello\"}}',\n )\n await asyncio.sleep(0.05)\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": True,\n \"stream_idle_timeout\": 0.01,\n \"timeout\": {\"connect\": 1.0, \"read\": 9.0, \"write\": 2.0, \"pool\": 3.0},\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert len(events) == 3\n assert events[0][0] == \"content_block_delta\"\n assert events[1][0] == \"status\"\n assert events[1][1][\"status\"] == \"failed\"\n assert events[1][1][\"retry\"] is False\n assert events[1][1][\"reason\"] == \"Stream idle timeout after 0.01 seconds.\"\n assert events[2][0] == \"error\"\n assert isinstance(events[2][1], RuntimeStageStallError)\n assert events[2][1].stage == \"response_stream\"\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_returns_stall_error(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json, headers\n await asyncio.sleep(10)\n raise AssertionError(\"post should have been cancelled by the idle deadline\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n statuses = [payload for event, payload in events if event == \"status\"]\n assert statuses\n assert statuses[-1][\"status\"] == \"failed\"\n assert any(\n \"Non-streaming response made no progress before idle deadline\" in str(payload.get(\"reason\") or \"\")\n for payload in statuses\n )\n assert events[-1][0] == \"error\"\n assert isinstance(events[-1][1], RuntimeStageStallError)\n assert events[-1][1].stage == \"response_materialization\"\n assert events[-1][1].timeout_seconds == 0.01\n assert \"stream_idle_timeout=0.01\" in str(events[-1][1])\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_allows_api_key_failover(monkeypatch: pytest.MonkeyPatch):\n calls: list[dict[str, str]] = []\n\n class FakeResponse:\n def __init__(self, status_code: int, content: bytes):\n self.status_code = status_code\n self.content = content\n self.text = content.decode()\n self.headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json\n calls.append(dict(self.headers if headers is None else headers))\n if len(calls) == 1:\n await asyncio.sleep(10)\n raise AssertionError(\"first post should have been cancelled by the idle deadline\")\n return FakeResponse(\n 200,\n b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n )\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"api_key\": \"key-a\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n \"_api_key_pool_runtime\": {\n \"pool_id\": \"example\",\n \"failover\": {\"handler\": lambda _context: \"try_next\", \"max_attempts\": 2},\n \"keys\": [\n {\"id\": \"a\", \"value\": \"key-a\", \"index\": 0},\n {\"id\": \"b\", \"value\": \"key-b\", \"index\": 1},\n ],\n \"selected_key_id\": \"a\",\n \"attempts\": [{\"key_id\": \"a\", \"action\": \"initial\"}],\n },\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert [headers.get(\"x-api-key\") for headers in calls] == [\"key-a\", \"key-b\"]\n assert events == [\n (\n \"message\",\n '{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ]\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_preserves_core_status_record():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})],\n )\n\n assert events == [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})]\n\n\ndef test_broadcast_response_resets_attempt_text_after_retry_status():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"partial\"}}),\n ),\n (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"server disconnected\",\n },\n ),\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"replacement\"}}),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert [payload for event, payload in events if event == \"delta\"] == [\"partial\", \"replacement\"]\n assert (\"done\", \"replacement\") in events\n assert (\"done\", \"partialreplacement\") not in events\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "b0d45a1341ef45debe90825c213e36979241e677": {"t": 1785753148.7586284, "n": 21136, "h": "6832ca3785c4c2c5", "c": "import asyncio\nimport json\nfrom types import SimpleNamespace\nfrom typing import Any\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.application.AgentExecution import RuntimeStageStallError\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, Any]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\n@pytest.mark.asyncio\nasync def test_stream_idle_timeout_returns_timeout_error_event(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def aclose(self):\n return None\n\n async def fake_aiter_sse_with_retry(self, client, method, url, *, headers, json):\n del self, client, method, url, headers, json\n\n async def generator():\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"hello\"}}',\n )\n await asyncio.sleep(0.05)\n yield SimpleNamespace(\n event=\"content_block_delta\",\n data='{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"late\"}}',\n )\n\n return generator()\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n monkeypatch.setattr(AnthropicCompatible, \"_aiter_sse_with_retry\", fake_aiter_sse_with_retry)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": True,\n \"stream_idle_timeout\": 0.01,\n \"timeout\": {\"connect\": 1.0, \"read\": 9.0, \"write\": 2.0, \"pool\": 3.0},\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert len(events) == 3\n assert events[0][0] == \"content_block_delta\"\n assert events[1][0] == \"status\"\n assert events[1][1][\"status\"] == \"failed\"\n assert events[1][1][\"retry\"] is False\n assert events[1][1][\"reason\"] == \"Stream idle timeout after 0.01 seconds.\"\n assert events[2][0] == \"error\"\n assert isinstance(events[2][1], RuntimeStageStallError)\n assert events[2][1].stage == \"response_stream\"\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_returns_stall_error(monkeypatch: pytest.MonkeyPatch):\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json, headers\n await asyncio.sleep(10)\n raise AssertionError(\"post should have been cancelled by the idle deadline\")\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n statuses = [payload for event, payload in events if event == \"status\"]\n assert statuses\n assert statuses[-1][\"status\"] == \"failed\"\n assert any(\n \"Non-streaming response made no progress before idle deadline\" in str(payload.get(\"reason\") or \"\")\n for payload in statuses\n )\n assert events[-1][0] == \"error\"\n assert isinstance(events[-1][1], RuntimeStageStallError)\n assert events[-1][1].stage == \"response_materialization\"\n assert events[-1][1].timeout_seconds == 0.01\n assert \"stream_idle_timeout=0.01\" in str(events[-1][1])\n\n\n@pytest.mark.asyncio\nasync def test_non_stream_response_idle_timeout_allows_api_key_failover(monkeypatch: pytest.MonkeyPatch):\n calls: list[dict[str, str]] = []\n\n class FakeResponse:\n def __init__(self, status_code: int, content: bytes):\n self.status_code = status_code\n self.content = content\n self.text = content.decode()\n self.headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n del url, json\n calls.append(dict(self.headers if headers is None else headers))\n if len(calls) == 1:\n await asyncio.sleep(10)\n raise AssertionError(\"first post should have been cancelled by the idle deadline\")\n return FakeResponse(\n 200,\n b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n )\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n\n plugin = build_plugin(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"api_key\": \"key-a\",\n \"stream\": False,\n \"stream_idle_timeout\": 0.01,\n \"_api_key_pool_runtime\": {\n \"pool_id\": \"example\",\n \"failover\": {\"handler\": lambda _context: \"try_next\", \"max_attempts\": 2},\n \"keys\": [\n {\"id\": \"a\", \"value\": \"key-a\", \"index\": 0},\n {\"id\": \"b\", \"value\": \"key-b\", \"index\": 1},\n ],\n \"selected_key_id\": \"a\",\n \"attempts\": [{\"key_id\": \"a\", \"action\": \"initial\"}],\n },\n },\n {\"input\": \"hello\"},\n )\n\n events = []\n async for event, payload in plugin.request_model(plugin.generate_request_data()):\n events.append((event, payload))\n\n assert [headers.get(\"x-api-key\") for headers in calls] == [\"key-a\", \"key-b\"]\n assert events == [\n (\n \"message\",\n '{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"ok\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}',\n ),\n (\"status\", {\"status\": \"completed\", \"attempt_index\": 1, \"retry\": False}),\n ]\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_preserves_core_status_record():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})],\n )\n\n assert events == [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})]\n\n\ndef test_broadcast_response_resets_attempt_text_after_retry_status():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"partial\"}}),\n ),\n (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"server disconnected\",\n },\n ),\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"replacement\"}}),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert [payload for event, payload in events if event == \"delta\"] == [\"partial\", \"replacement\"]\n assert (\"done\", \"replacement\") in events\n assert (\"done\", \"partialreplacement\") not in events\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "2ddc87f657ff28160bcc9e4d2cc3ea494cbc05d6": {"t": 1785753149.925212, "n": 14542, "h": "f93a692cd9a09d2f", "c": "import asyncio\nimport json\nfrom typing import Any\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, Any]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_preserves_core_status_record():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})],\n )\n\n assert events == [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})]\n\n\ndef test_broadcast_response_resets_attempt_text_after_retry_status():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"partial\"}}),\n ),\n (\n \"status\",\n {\n \"status\": \"failed\",\n \"attempt_index\": 1,\n \"retry\": True,\n \"next_attempt_index\": 2,\n \"reason\": \"server disconnected\",\n },\n ),\n (\n \"content_block_start\",\n json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}),\n ),\n (\n \"content_block_delta\",\n json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"replacement\"}}),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert [payload for event, payload in events if event == \"delta\"] == [\"partial\", \"replacement\"]\n assert (\"done\", \"replacement\") in events\n assert (\"done\", \"partialreplacement\") not in events\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "bbd8533f8a8e304959ec5b0fe586430335b8e8ba": {"t": 1785753151.0947645, "n": 12868, "h": "0f01d25b52f32984", "c": "import asyncio\nimport json\nfrom typing import Any\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, Any]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_preserves_core_status_record():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n events = collect_events(\n plugin,\n [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})],\n )\n\n assert events == [(\"status\", {\"status\": \"failed\", \"attempt_index\": 1, \"retry\": True})]\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "948ec648f45d173925d8f0cc35e77ee9c73802df": {"t": 1785753152.8612792, "n": 12464, "h": "0ebf66e739a7e839", "c": "import asyncio\nimport json\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, str]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_generate_request_maps_data_url_image_to_base64_source():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"attachment\": [\n {\n \"type\": \"image_url\",\n \"image_url\": {\"url\": \"data:image/png;base64,aGVsbG8=\"},\n }\n ],\n },\n )\n\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0] == {\n \"type\": \"image\",\n \"source\": {\n \"type\": \"base64\",\n \"media_type\": \"image/png\",\n \"data\": \"aGVsbG8=\",\n },\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "651d1bfd65fbabf1627877083a0fd4f3f47089fc": {"t": 1785753154.125847, "n": 11813, "h": "f0a92798cf61d19b", "c": "import asyncio\nimport json\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.model.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, str]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "cd3a970b2fd288360855b5694a1c52b96d4662cc": {"t": 1785753156.4366782, "n": 11807, "h": "d8b567b0901c4f4c", "c": "import asyncio\nimport json\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, str]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert ModelRequester in AnthropicCompatible.__mro__\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "a286a93e4f8adcb906b4538772a13313bd64fad5": {"t": 1785753157.992166, "n": 11812, "h": "f90c70d5198e913d", "c": "import asyncio\nimport json\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, str]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_client_options_disable_environment_proxy_by_default():\n request = generate_request({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n\n assert request[\"client_options\"][\"trust_env\"] is False\n\n\ndef test_client_options_can_enable_environment_proxy_explicitly():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"client_options\": {\"trust_env\": True},\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"client_options\"][\"trust_env\"] is True\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert AnthropicCompatible.__bases__ == (ModelRequester,)\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "08a72ba0f5f31346fa78a15a26ebbc8f0113f5eb": {"t": 1785753159.2337997, "n": 11259, "h": "a594353a6d8793d8", "c": "import asyncio\nimport json\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, str]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 8192\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert AnthropicCompatible.__bases__ == (ModelRequester,)\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "395ffb32daac799d9992ecb3a76e5d654d34d9bf": {"t": 1785753160.5254853, "n": 11259, "h": "d22f8683b3315c0b", "c": "import asyncio\nimport json\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nfrom agently.types.plugins import ModelRequester\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, str]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 4096\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_inherits_model_requester_protocol_instead_of_responses_plugin():\n assert AnthropicCompatible.__bases__ == (ModelRequester,)\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}, "efe05381f9d308d0b0193ed7aaa50be51edf9545": {"t": 1785753161.71567, "n": 11072, "h": "3384fd520b76ff23", "c": "import asyncio\nimport json\n\nimport pytest\n\nfrom agently import Agently\nfrom agently.core.Prompt import Prompt\nfrom agently.utils import Settings\nfrom agently.builtins.plugins.ModelRequester.AnthropicCompatible import (\n AnthropicCompatible,\n)\nimport agently.builtins.plugins.ModelRequester.AnthropicCompatible as anthropic_module\n\n\ndef build_plugin(config: dict, prompt_values: dict | None = None):\n settings = Settings(parent=Agently.settings)\n settings.update({\"plugins\": {\"ModelRequester\": {\"AnthropicCompatible\": config}}})\n prompt = Prompt(plugin_manager=Agently.plugin_manager, parent_settings=settings)\n for key, value in (prompt_values or {}).items():\n prompt.set(key, value)\n return AnthropicCompatible(prompt, settings)\n\n\ndef generate_request(config: dict, prompt_values: dict | None = None):\n return build_plugin(config, prompt_values).generate_request_data().model_dump()\n\n\nasync def capture_request_headers(monkeypatch: pytest.MonkeyPatch, config: dict, prompt_values: dict | None = None):\n captured: dict = {}\n\n class FakeResponse:\n status_code = 200\n content = b'{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[{\"type\":\"text\",\"text\":\"hello\"}],\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n text = content.decode()\n headers = {\"Content-Type\": \"application/json\"}\n\n class FakeAsyncClient:\n def __init__(self, **kwargs):\n self.headers = {}\n captured[\"client_kwargs\"] = kwargs\n\n async def __aenter__(self):\n return self\n\n async def __aexit__(self, exc_type, exc, tb):\n return False\n\n async def post(self, url, json=None, headers=None):\n captured[\"url\"] = url\n captured[\"json\"] = json\n captured[\"headers\"] = dict(self.headers if headers is None else headers)\n return FakeResponse()\n\n async def aclose(self):\n return None\n\n monkeypatch.setattr(anthropic_module, \"AsyncClient\", FakeAsyncClient)\n plugin = build_plugin(config, prompt_values)\n request_data = plugin.generate_request_data()\n async for _event, _payload in plugin.request_model(request_data):\n pass\n return captured\n\n\ndef collect_events(plugin: AnthropicCompatible, request_events: list[tuple[str, str]]):\n async def _run():\n async def generator():\n for event, payload in request_events:\n yield event, payload\n\n collected = []\n async for event, data in plugin.broadcast_response(generator()):\n collected.append((event, data))\n return collected\n\n return asyncio.run(_run())\n\n\ndef test_generate_request_uses_messages_path_and_default_model():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\"input\": \"hello\"},\n )\n\n assert request[\"request_url\"] == \"https://api.anthropic.example/v1/messages\"\n assert request[\"request_options\"][\"model\"] == \"claude-sonnet-4-20250514\"\n assert request[\"request_options\"][\"stream\"] is True\n assert request[\"request_options\"][\"max_tokens\"] == 4096\n assert request[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert request[\"data\"][\"messages\"] == [{\"role\": \"user\", \"content\": \"hello\"}]\n\n\ndef test_generate_request_maps_system_and_rich_content():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n },\n {\n \"instruct\": \"Be concise.\",\n \"input\": \"Describe this image.\",\n \"attachment\": [{\"type\": \"image_url\", \"image_url\": \"https://example.com/cat.png\"}],\n },\n )\n\n assert \"system\" not in request[\"data\"]\n assert request[\"data\"][\"messages\"][0][\"role\"] == \"user\"\n content = request[\"data\"][\"messages\"][0][\"content\"]\n assert content[0][\"type\"] == \"text\"\n assert \"[INSTRUCT]:\" in content[0][\"text\"]\n assert \"Be concise.\" in content[0][\"text\"]\n assert \"Describe this image.\" in content[0][\"text\"]\n assert content[1] == {\n \"type\": \"image\",\n \"source\": {\"type\": \"url\", \"url\": \"https://example.com/cat.png\"},\n }\n\n\ndef test_prompt_tools_are_converted_and_explicit_tools_override_by_name():\n request = generate_request(\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"request_options\": {\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"description\": \"explicit override\",\n \"input_schema\": {\"type\": \"object\", \"properties\": {\"query\": {\"type\": \"string\"}}},\n }\n ]\n },\n },\n {\n \"input\": \"Find docs.\",\n \"tools\": [\n {\n \"name\": \"search_docs\",\n \"desc\": \"Search the documentation.\",\n \"kwargs\": {\"query\": (str, \"query text\"), \"limit\": (int, \"max result count\")},\n },\n {\n \"name\": \"lookup_issue\",\n \"desc\": \"Lookup a GitHub issue.\",\n \"kwargs\": {\"issue_id\": (str, \"Issue id\")},\n },\n ],\n },\n )\n\n tools = request[\"request_options\"][\"tools\"]\n search_docs = next(tool for tool in tools if tool.get(\"name\") == \"search_docs\")\n lookup_issue = next(tool for tool in tools if tool.get(\"name\") == \"lookup_issue\")\n\n assert search_docs[\"description\"] == \"explicit override\"\n assert lookup_issue[\"input_schema\"][\"properties\"][\"issue_id\"][\"type\"] == \"string\"\n assert lookup_issue[\"input_schema\"][\"additionalProperties\"] is False\n\n\n@pytest.mark.asyncio\nasync def test_auth_headers_are_preserved_in_outgoing_request(monkeypatch: pytest.MonkeyPatch):\n captured = await capture_request_headers(\n monkeypatch,\n {\n \"base_url\": \"https://api.anthropic.example/v1\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"stream\": False,\n \"auth\": {\"headers\": {\"X-Test\": \"1\"}, \"api_key\": \"claude-secret\"},\n \"anthropic_beta\": [\"tools-2024-04-04\"],\n },\n {\"input\": \"hello\"},\n )\n\n assert captured[\"headers\"][\"x-api-key\"] == \"claude-secret\"\n assert captured[\"headers\"][\"X-Test\"] == \"1\"\n assert captured[\"headers\"][\"anthropic-version\"] == \"2023-06-01\"\n assert captured[\"headers\"][\"anthropic-beta\"] == \"tools-2024-04-04\"\n\n\ndef test_broadcast_response_maps_text_stream_and_meta():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"hello\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_1\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\"content_block_start\", json.dumps({\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"Hello \"}})),\n (\"content_block_delta\", json.dumps({\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"world\"}})),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"end_turn\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 1, \"output_tokens\": 2},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n assert (\"delta\", \"Hello \") in events\n assert (\"delta\", \"world\") in events\n assert (\"done\", \"Hello world\") in events\n assert (\"reasoning_done\", \"\") in events\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"id\"] == \"msg_1\"\n assert meta[\"finish_reason\"] == \"stop\"\n assert meta[\"usage\"][\"output_tokens\"] == 2\n\n\ndef test_broadcast_response_maps_tool_use_to_tool_calls():\n plugin = build_plugin({\"base_url\": \"https://api.anthropic.example/v1\"}, {\"input\": \"Find docs\"})\n events = collect_events(\n plugin,\n [\n (\n \"message_start\",\n json.dumps(\n {\n \"type\": \"message_start\",\n \"message\": {\n \"id\": \"msg_2\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"model\": \"claude-sonnet-4-20250514\",\n \"content\": [],\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 0},\n },\n }\n ),\n ),\n (\n \"content_block_start\",\n json.dumps(\n {\n \"type\": \"content_block_start\",\n \"index\": 0,\n \"content_block\": {\"type\": \"tool_use\", \"id\": \"toolu_1\", \"name\": \"search_docs\", \"input\": {}},\n }\n ),\n ),\n (\n \"content_block_delta\",\n json.dumps(\n {\n \"type\": \"content_block_delta\",\n \"index\": 0,\n \"delta\": {\"type\": \"input_json_delta\", \"partial_json\": \"{\\\"query\\\":\\\"anthropic\\\"}\"},\n }\n ),\n ),\n (\"content_block_stop\", json.dumps({\"type\": \"content_block_stop\", \"index\": 0})),\n (\n \"message_delta\",\n json.dumps(\n {\n \"type\": \"message_delta\",\n \"delta\": {\"stop_reason\": \"tool_use\", \"stop_sequence\": None},\n \"usage\": {\"input_tokens\": 3, \"output_tokens\": 4},\n }\n ),\n ),\n (\"message_stop\", json.dumps({\"type\": \"message_stop\"})),\n ],\n )\n\n tool_call_event = next(data for event, data in events if event == \"tool_calls\")\n assert tool_call_event[\"id\"] == \"toolu_1\"\n assert tool_call_event[\"function\"][\"name\"] == \"search_docs\"\n assert tool_call_event[\"function\"][\"arguments\"] == \"{\\\"query\\\":\\\"anthropic\\\"}\"\n meta = next(data for event, data in events if event == \"meta\")\n assert meta[\"finish_reason\"] == \"tool_calls\"\n"}}}}