agno 2.2.13__py3-none-any.whl → 2.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. agno/agent/__init__.py +6 -0
  2. agno/agent/agent.py +5252 -3145
  3. agno/agent/remote.py +525 -0
  4. agno/api/api.py +2 -0
  5. agno/client/__init__.py +3 -0
  6. agno/client/a2a/__init__.py +10 -0
  7. agno/client/a2a/client.py +554 -0
  8. agno/client/a2a/schemas.py +112 -0
  9. agno/client/a2a/utils.py +369 -0
  10. agno/client/os.py +2669 -0
  11. agno/compression/__init__.py +3 -0
  12. agno/compression/manager.py +247 -0
  13. agno/culture/manager.py +2 -2
  14. agno/db/base.py +927 -6
  15. agno/db/dynamo/dynamo.py +788 -2
  16. agno/db/dynamo/schemas.py +128 -0
  17. agno/db/dynamo/utils.py +26 -3
  18. agno/db/firestore/firestore.py +674 -50
  19. agno/db/firestore/schemas.py +41 -0
  20. agno/db/firestore/utils.py +25 -10
  21. agno/db/gcs_json/gcs_json_db.py +506 -3
  22. agno/db/gcs_json/utils.py +14 -2
  23. agno/db/in_memory/in_memory_db.py +203 -4
  24. agno/db/in_memory/utils.py +14 -2
  25. agno/db/json/json_db.py +498 -2
  26. agno/db/json/utils.py +14 -2
  27. agno/db/migrations/manager.py +199 -0
  28. agno/db/migrations/utils.py +19 -0
  29. agno/db/migrations/v1_to_v2.py +54 -16
  30. agno/db/migrations/versions/__init__.py +0 -0
  31. agno/db/migrations/versions/v2_3_0.py +977 -0
  32. agno/db/mongo/async_mongo.py +1013 -39
  33. agno/db/mongo/mongo.py +684 -4
  34. agno/db/mongo/schemas.py +48 -0
  35. agno/db/mongo/utils.py +17 -0
  36. agno/db/mysql/__init__.py +2 -1
  37. agno/db/mysql/async_mysql.py +2958 -0
  38. agno/db/mysql/mysql.py +722 -53
  39. agno/db/mysql/schemas.py +77 -11
  40. agno/db/mysql/utils.py +151 -8
  41. agno/db/postgres/async_postgres.py +1254 -137
  42. agno/db/postgres/postgres.py +2316 -93
  43. agno/db/postgres/schemas.py +153 -21
  44. agno/db/postgres/utils.py +22 -7
  45. agno/db/redis/redis.py +531 -3
  46. agno/db/redis/schemas.py +36 -0
  47. agno/db/redis/utils.py +31 -15
  48. agno/db/schemas/evals.py +1 -0
  49. agno/db/schemas/memory.py +20 -9
  50. agno/db/singlestore/schemas.py +70 -1
  51. agno/db/singlestore/singlestore.py +737 -74
  52. agno/db/singlestore/utils.py +13 -3
  53. agno/db/sqlite/async_sqlite.py +1069 -89
  54. agno/db/sqlite/schemas.py +133 -1
  55. agno/db/sqlite/sqlite.py +2203 -165
  56. agno/db/sqlite/utils.py +21 -11
  57. agno/db/surrealdb/models.py +25 -0
  58. agno/db/surrealdb/surrealdb.py +603 -1
  59. agno/db/utils.py +60 -0
  60. agno/eval/__init__.py +26 -3
  61. agno/eval/accuracy.py +25 -12
  62. agno/eval/agent_as_judge.py +871 -0
  63. agno/eval/base.py +29 -0
  64. agno/eval/performance.py +10 -4
  65. agno/eval/reliability.py +22 -13
  66. agno/eval/utils.py +2 -1
  67. agno/exceptions.py +42 -0
  68. agno/hooks/__init__.py +3 -0
  69. agno/hooks/decorator.py +164 -0
  70. agno/integrations/discord/client.py +13 -2
  71. agno/knowledge/__init__.py +4 -0
  72. agno/knowledge/chunking/code.py +90 -0
  73. agno/knowledge/chunking/document.py +65 -4
  74. agno/knowledge/chunking/fixed.py +4 -1
  75. agno/knowledge/chunking/markdown.py +102 -11
  76. agno/knowledge/chunking/recursive.py +2 -2
  77. agno/knowledge/chunking/semantic.py +130 -48
  78. agno/knowledge/chunking/strategy.py +18 -0
  79. agno/knowledge/embedder/azure_openai.py +0 -1
  80. agno/knowledge/embedder/google.py +1 -1
  81. agno/knowledge/embedder/mistral.py +1 -1
  82. agno/knowledge/embedder/nebius.py +1 -1
  83. agno/knowledge/embedder/openai.py +16 -12
  84. agno/knowledge/filesystem.py +412 -0
  85. agno/knowledge/knowledge.py +4261 -1199
  86. agno/knowledge/protocol.py +134 -0
  87. agno/knowledge/reader/arxiv_reader.py +3 -2
  88. agno/knowledge/reader/base.py +9 -7
  89. agno/knowledge/reader/csv_reader.py +91 -42
  90. agno/knowledge/reader/docx_reader.py +9 -10
  91. agno/knowledge/reader/excel_reader.py +225 -0
  92. agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
  93. agno/knowledge/reader/firecrawl_reader.py +3 -2
  94. agno/knowledge/reader/json_reader.py +16 -22
  95. agno/knowledge/reader/markdown_reader.py +15 -14
  96. agno/knowledge/reader/pdf_reader.py +33 -28
  97. agno/knowledge/reader/pptx_reader.py +9 -10
  98. agno/knowledge/reader/reader_factory.py +135 -1
  99. agno/knowledge/reader/s3_reader.py +8 -16
  100. agno/knowledge/reader/tavily_reader.py +3 -3
  101. agno/knowledge/reader/text_reader.py +15 -14
  102. agno/knowledge/reader/utils/__init__.py +17 -0
  103. agno/knowledge/reader/utils/spreadsheet.py +114 -0
  104. agno/knowledge/reader/web_search_reader.py +8 -65
  105. agno/knowledge/reader/website_reader.py +16 -13
  106. agno/knowledge/reader/wikipedia_reader.py +36 -3
  107. agno/knowledge/reader/youtube_reader.py +3 -2
  108. agno/knowledge/remote_content/__init__.py +33 -0
  109. agno/knowledge/remote_content/config.py +266 -0
  110. agno/knowledge/remote_content/remote_content.py +105 -17
  111. agno/knowledge/utils.py +76 -22
  112. agno/learn/__init__.py +71 -0
  113. agno/learn/config.py +463 -0
  114. agno/learn/curate.py +185 -0
  115. agno/learn/machine.py +725 -0
  116. agno/learn/schemas.py +1114 -0
  117. agno/learn/stores/__init__.py +38 -0
  118. agno/learn/stores/decision_log.py +1156 -0
  119. agno/learn/stores/entity_memory.py +3275 -0
  120. agno/learn/stores/learned_knowledge.py +1583 -0
  121. agno/learn/stores/protocol.py +117 -0
  122. agno/learn/stores/session_context.py +1217 -0
  123. agno/learn/stores/user_memory.py +1495 -0
  124. agno/learn/stores/user_profile.py +1220 -0
  125. agno/learn/utils.py +209 -0
  126. agno/media.py +22 -6
  127. agno/memory/__init__.py +14 -1
  128. agno/memory/manager.py +223 -8
  129. agno/memory/strategies/__init__.py +15 -0
  130. agno/memory/strategies/base.py +66 -0
  131. agno/memory/strategies/summarize.py +196 -0
  132. agno/memory/strategies/types.py +37 -0
  133. agno/models/aimlapi/aimlapi.py +17 -0
  134. agno/models/anthropic/claude.py +434 -59
  135. agno/models/aws/bedrock.py +121 -20
  136. agno/models/aws/claude.py +131 -274
  137. agno/models/azure/ai_foundry.py +10 -6
  138. agno/models/azure/openai_chat.py +33 -10
  139. agno/models/base.py +1162 -561
  140. agno/models/cerebras/cerebras.py +120 -24
  141. agno/models/cerebras/cerebras_openai.py +21 -2
  142. agno/models/cohere/chat.py +65 -6
  143. agno/models/cometapi/cometapi.py +18 -1
  144. agno/models/dashscope/dashscope.py +2 -3
  145. agno/models/deepinfra/deepinfra.py +18 -1
  146. agno/models/deepseek/deepseek.py +69 -3
  147. agno/models/fireworks/fireworks.py +18 -1
  148. agno/models/google/gemini.py +959 -89
  149. agno/models/google/utils.py +22 -0
  150. agno/models/groq/groq.py +48 -18
  151. agno/models/huggingface/huggingface.py +17 -6
  152. agno/models/ibm/watsonx.py +16 -6
  153. agno/models/internlm/internlm.py +18 -1
  154. agno/models/langdb/langdb.py +13 -1
  155. agno/models/litellm/chat.py +88 -9
  156. agno/models/litellm/litellm_openai.py +18 -1
  157. agno/models/message.py +24 -5
  158. agno/models/meta/llama.py +40 -13
  159. agno/models/meta/llama_openai.py +22 -21
  160. agno/models/metrics.py +12 -0
  161. agno/models/mistral/mistral.py +8 -4
  162. agno/models/n1n/__init__.py +3 -0
  163. agno/models/n1n/n1n.py +57 -0
  164. agno/models/nebius/nebius.py +6 -7
  165. agno/models/nvidia/nvidia.py +20 -3
  166. agno/models/ollama/__init__.py +2 -0
  167. agno/models/ollama/chat.py +17 -6
  168. agno/models/ollama/responses.py +100 -0
  169. agno/models/openai/__init__.py +2 -0
  170. agno/models/openai/chat.py +117 -26
  171. agno/models/openai/open_responses.py +46 -0
  172. agno/models/openai/responses.py +110 -32
  173. agno/models/openrouter/__init__.py +2 -0
  174. agno/models/openrouter/openrouter.py +67 -2
  175. agno/models/openrouter/responses.py +146 -0
  176. agno/models/perplexity/perplexity.py +19 -1
  177. agno/models/portkey/portkey.py +7 -6
  178. agno/models/requesty/requesty.py +19 -2
  179. agno/models/response.py +20 -2
  180. agno/models/sambanova/sambanova.py +20 -3
  181. agno/models/siliconflow/siliconflow.py +19 -2
  182. agno/models/together/together.py +20 -3
  183. agno/models/vercel/v0.py +20 -3
  184. agno/models/vertexai/claude.py +124 -4
  185. agno/models/vllm/vllm.py +19 -14
  186. agno/models/xai/xai.py +19 -2
  187. agno/os/app.py +467 -137
  188. agno/os/auth.py +253 -5
  189. agno/os/config.py +22 -0
  190. agno/os/interfaces/a2a/a2a.py +7 -6
  191. agno/os/interfaces/a2a/router.py +635 -26
  192. agno/os/interfaces/a2a/utils.py +32 -33
  193. agno/os/interfaces/agui/agui.py +5 -3
  194. agno/os/interfaces/agui/router.py +26 -16
  195. agno/os/interfaces/agui/utils.py +97 -57
  196. agno/os/interfaces/base.py +7 -7
  197. agno/os/interfaces/slack/router.py +16 -7
  198. agno/os/interfaces/slack/slack.py +7 -7
  199. agno/os/interfaces/whatsapp/router.py +35 -7
  200. agno/os/interfaces/whatsapp/security.py +3 -1
  201. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  202. agno/os/managers.py +326 -0
  203. agno/os/mcp.py +652 -79
  204. agno/os/middleware/__init__.py +4 -0
  205. agno/os/middleware/jwt.py +718 -115
  206. agno/os/middleware/trailing_slash.py +27 -0
  207. agno/os/router.py +105 -1558
  208. agno/os/routers/agents/__init__.py +3 -0
  209. agno/os/routers/agents/router.py +655 -0
  210. agno/os/routers/agents/schema.py +288 -0
  211. agno/os/routers/components/__init__.py +3 -0
  212. agno/os/routers/components/components.py +475 -0
  213. agno/os/routers/database.py +155 -0
  214. agno/os/routers/evals/evals.py +111 -18
  215. agno/os/routers/evals/schemas.py +38 -5
  216. agno/os/routers/evals/utils.py +80 -11
  217. agno/os/routers/health.py +3 -3
  218. agno/os/routers/knowledge/knowledge.py +284 -35
  219. agno/os/routers/knowledge/schemas.py +14 -2
  220. agno/os/routers/memory/memory.py +274 -11
  221. agno/os/routers/memory/schemas.py +44 -3
  222. agno/os/routers/metrics/metrics.py +30 -15
  223. agno/os/routers/metrics/schemas.py +10 -6
  224. agno/os/routers/registry/__init__.py +3 -0
  225. agno/os/routers/registry/registry.py +337 -0
  226. agno/os/routers/session/session.py +143 -14
  227. agno/os/routers/teams/__init__.py +3 -0
  228. agno/os/routers/teams/router.py +550 -0
  229. agno/os/routers/teams/schema.py +280 -0
  230. agno/os/routers/traces/__init__.py +3 -0
  231. agno/os/routers/traces/schemas.py +414 -0
  232. agno/os/routers/traces/traces.py +549 -0
  233. agno/os/routers/workflows/__init__.py +3 -0
  234. agno/os/routers/workflows/router.py +757 -0
  235. agno/os/routers/workflows/schema.py +139 -0
  236. agno/os/schema.py +157 -584
  237. agno/os/scopes.py +469 -0
  238. agno/os/settings.py +3 -0
  239. agno/os/utils.py +574 -185
  240. agno/reasoning/anthropic.py +85 -1
  241. agno/reasoning/azure_ai_foundry.py +93 -1
  242. agno/reasoning/deepseek.py +102 -2
  243. agno/reasoning/default.py +6 -7
  244. agno/reasoning/gemini.py +87 -3
  245. agno/reasoning/groq.py +109 -2
  246. agno/reasoning/helpers.py +6 -7
  247. agno/reasoning/manager.py +1238 -0
  248. agno/reasoning/ollama.py +93 -1
  249. agno/reasoning/openai.py +115 -1
  250. agno/reasoning/vertexai.py +85 -1
  251. agno/registry/__init__.py +3 -0
  252. agno/registry/registry.py +68 -0
  253. agno/remote/__init__.py +3 -0
  254. agno/remote/base.py +581 -0
  255. agno/run/__init__.py +2 -4
  256. agno/run/agent.py +134 -19
  257. agno/run/base.py +49 -1
  258. agno/run/cancel.py +65 -52
  259. agno/run/cancellation_management/__init__.py +9 -0
  260. agno/run/cancellation_management/base.py +78 -0
  261. agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
  262. agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
  263. agno/run/requirement.py +181 -0
  264. agno/run/team.py +111 -19
  265. agno/run/workflow.py +2 -1
  266. agno/session/agent.py +57 -92
  267. agno/session/summary.py +1 -1
  268. agno/session/team.py +62 -115
  269. agno/session/workflow.py +353 -57
  270. agno/skills/__init__.py +17 -0
  271. agno/skills/agent_skills.py +377 -0
  272. agno/skills/errors.py +32 -0
  273. agno/skills/loaders/__init__.py +4 -0
  274. agno/skills/loaders/base.py +27 -0
  275. agno/skills/loaders/local.py +216 -0
  276. agno/skills/skill.py +65 -0
  277. agno/skills/utils.py +107 -0
  278. agno/skills/validator.py +277 -0
  279. agno/table.py +10 -0
  280. agno/team/__init__.py +5 -1
  281. agno/team/remote.py +447 -0
  282. agno/team/team.py +3769 -2202
  283. agno/tools/brandfetch.py +27 -18
  284. agno/tools/browserbase.py +225 -16
  285. agno/tools/crawl4ai.py +3 -0
  286. agno/tools/duckduckgo.py +25 -71
  287. agno/tools/exa.py +0 -21
  288. agno/tools/file.py +14 -13
  289. agno/tools/file_generation.py +12 -6
  290. agno/tools/firecrawl.py +15 -7
  291. agno/tools/function.py +94 -113
  292. agno/tools/google_bigquery.py +11 -2
  293. agno/tools/google_drive.py +4 -3
  294. agno/tools/knowledge.py +9 -4
  295. agno/tools/mcp/mcp.py +301 -18
  296. agno/tools/mcp/multi_mcp.py +269 -14
  297. agno/tools/mem0.py +11 -10
  298. agno/tools/memory.py +47 -46
  299. agno/tools/mlx_transcribe.py +10 -7
  300. agno/tools/models/nebius.py +5 -5
  301. agno/tools/models_labs.py +20 -10
  302. agno/tools/nano_banana.py +151 -0
  303. agno/tools/parallel.py +0 -7
  304. agno/tools/postgres.py +76 -36
  305. agno/tools/python.py +14 -6
  306. agno/tools/reasoning.py +30 -23
  307. agno/tools/redshift.py +406 -0
  308. agno/tools/shopify.py +1519 -0
  309. agno/tools/spotify.py +919 -0
  310. agno/tools/tavily.py +4 -1
  311. agno/tools/toolkit.py +253 -18
  312. agno/tools/websearch.py +93 -0
  313. agno/tools/website.py +1 -1
  314. agno/tools/wikipedia.py +1 -1
  315. agno/tools/workflow.py +56 -48
  316. agno/tools/yfinance.py +12 -11
  317. agno/tracing/__init__.py +12 -0
  318. agno/tracing/exporter.py +161 -0
  319. agno/tracing/schemas.py +276 -0
  320. agno/tracing/setup.py +112 -0
  321. agno/utils/agent.py +251 -10
  322. agno/utils/cryptography.py +22 -0
  323. agno/utils/dttm.py +33 -0
  324. agno/utils/events.py +264 -7
  325. agno/utils/hooks.py +111 -3
  326. agno/utils/http.py +161 -2
  327. agno/utils/mcp.py +49 -8
  328. agno/utils/media.py +22 -1
  329. agno/utils/models/ai_foundry.py +9 -2
  330. agno/utils/models/claude.py +20 -5
  331. agno/utils/models/cohere.py +9 -2
  332. agno/utils/models/llama.py +9 -2
  333. agno/utils/models/mistral.py +4 -2
  334. agno/utils/os.py +0 -0
  335. agno/utils/print_response/agent.py +99 -16
  336. agno/utils/print_response/team.py +223 -24
  337. agno/utils/print_response/workflow.py +0 -2
  338. agno/utils/prompts.py +8 -6
  339. agno/utils/remote.py +23 -0
  340. agno/utils/response.py +1 -13
  341. agno/utils/string.py +91 -2
  342. agno/utils/team.py +62 -12
  343. agno/utils/tokens.py +657 -0
  344. agno/vectordb/base.py +15 -2
  345. agno/vectordb/cassandra/cassandra.py +1 -1
  346. agno/vectordb/chroma/__init__.py +2 -1
  347. agno/vectordb/chroma/chromadb.py +468 -23
  348. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  349. agno/vectordb/couchbase/couchbase.py +6 -2
  350. agno/vectordb/lancedb/lance_db.py +7 -38
  351. agno/vectordb/lightrag/lightrag.py +7 -6
  352. agno/vectordb/milvus/milvus.py +118 -84
  353. agno/vectordb/mongodb/__init__.py +2 -1
  354. agno/vectordb/mongodb/mongodb.py +14 -31
  355. agno/vectordb/pgvector/pgvector.py +120 -66
  356. agno/vectordb/pineconedb/pineconedb.py +2 -19
  357. agno/vectordb/qdrant/__init__.py +2 -1
  358. agno/vectordb/qdrant/qdrant.py +33 -56
  359. agno/vectordb/redis/__init__.py +2 -1
  360. agno/vectordb/redis/redisdb.py +19 -31
  361. agno/vectordb/singlestore/singlestore.py +17 -9
  362. agno/vectordb/surrealdb/surrealdb.py +2 -38
  363. agno/vectordb/weaviate/__init__.py +2 -1
  364. agno/vectordb/weaviate/weaviate.py +7 -3
  365. agno/workflow/__init__.py +5 -1
  366. agno/workflow/agent.py +2 -2
  367. agno/workflow/condition.py +12 -10
  368. agno/workflow/loop.py +28 -9
  369. agno/workflow/parallel.py +21 -13
  370. agno/workflow/remote.py +362 -0
  371. agno/workflow/router.py +12 -9
  372. agno/workflow/step.py +261 -36
  373. agno/workflow/steps.py +12 -8
  374. agno/workflow/types.py +40 -77
  375. agno/workflow/workflow.py +939 -213
  376. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
  377. agno-2.4.3.dist-info/RECORD +677 -0
  378. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
  379. agno/tools/googlesearch.py +0 -98
  380. agno/tools/memori.py +0 -339
  381. agno-2.2.13.dist-info/RECORD +0 -575
  382. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
  383. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,8 @@ from dataclasses import asdict, dataclass
4
4
  from os import getenv
5
5
  from typing import Any, Dict, List, Optional, Type, Union
6
6
 
7
- from pydantic import BaseModel
7
+ import httpx
8
+ from pydantic import BaseModel, ValidationError
8
9
 
9
10
  from agno.exceptions import ModelProviderError, ModelRateLimitError
10
11
  from agno.models.base import Model
@@ -12,8 +13,11 @@ from agno.models.message import Citations, DocumentCitation, Message, UrlCitatio
12
13
  from agno.models.metrics import Metrics
13
14
  from agno.models.response import ModelResponse
14
15
  from agno.run.agent import RunOutput
16
+ from agno.tools.function import Function
17
+ from agno.utils.http import get_default_async_client, get_default_sync_client
15
18
  from agno.utils.log import log_debug, log_error, log_warning
16
19
  from agno.utils.models.claude import MCPServerConfiguration, format_messages, format_tools_for_model
20
+ from agno.utils.tokens import count_schema_tokens
17
21
 
18
22
  try:
19
23
  from anthropic import Anthropic as AnthropicClient
@@ -25,6 +29,11 @@ try:
25
29
  from anthropic import (
26
30
  AsyncAnthropic as AsyncAnthropicClient,
27
31
  )
32
+ from anthropic.lib.streaming._beta_types import (
33
+ BetaRawContentBlockStartEvent,
34
+ ParsedBetaContentBlockStopEvent,
35
+ ParsedBetaMessageStopEvent,
36
+ )
28
37
  from anthropic.types import (
29
38
  CitationPageLocation,
30
39
  CitationsWebSearchResultLocation,
@@ -39,6 +48,7 @@ try:
39
48
  from anthropic.types import (
40
49
  Message as AnthropicMessage,
41
50
  )
51
+
42
52
  except ImportError as e:
43
53
  raise ImportError("`anthropic` not installed. Please install it with `pip install anthropic`") from e
44
54
 
@@ -72,6 +82,30 @@ class Claude(Model):
72
82
  "claude-3-5-haiku-latest",
73
83
  }
74
84
 
85
+ # Models that DO NOT support native structured outputs
86
+ # All future models are assumed to support structured outputs
87
+ NON_STRUCTURED_OUTPUT_MODELS = {
88
+ # Claude 3.x family (all versions)
89
+ "claude-3-opus-20240229",
90
+ "claude-3-sonnet-20240229",
91
+ "claude-3-haiku-20240307",
92
+ "claude-3-opus",
93
+ "claude-3-sonnet",
94
+ "claude-3-haiku",
95
+ # Claude 3.5 family (all versions except Sonnet 4.5)
96
+ "claude-3-5-sonnet-20240620",
97
+ "claude-3-5-sonnet-20241022",
98
+ "claude-3-5-sonnet",
99
+ "claude-3-5-haiku-20241022",
100
+ "claude-3-5-haiku-latest",
101
+ "claude-3-5-haiku",
102
+ # Claude Sonnet 4.x family (versions before 4.5)
103
+ "claude-sonnet-4-20250514",
104
+ "claude-sonnet-4",
105
+ # Claude Opus 4.x family (versions before 4.1 and 4.5)
106
+ # (Add any Opus 4.x models released before 4.1/4.5 if they exist)
107
+ }
108
+
75
109
  id: str = "claude-sonnet-4-5-20250929"
76
110
  name: str = "Claude"
77
111
  provider: str = "Anthropic"
@@ -97,8 +131,10 @@ class Claude(Model):
97
131
 
98
132
  # Client parameters
99
133
  api_key: Optional[str] = None
134
+ auth_token: Optional[str] = None
100
135
  default_headers: Optional[Dict[str, Any]] = None
101
136
  timeout: Optional[float] = None
137
+ http_client: Optional[Union[httpx.Client, httpx.AsyncClient]] = None
102
138
  client_params: Optional[Dict[str, Any]] = None
103
139
 
104
140
  client: Optional[AnthropicClient] = None
@@ -109,6 +145,9 @@ class Claude(Model):
109
145
  # Validate thinking support immediately at model creation
110
146
  if self.thinking:
111
147
  self._validate_thinking_support()
148
+ # Set structured outputs capability flag for supported models
149
+ if self._supports_structured_outputs():
150
+ self.supports_native_structured_outputs = True
112
151
  # Set up skills configuration if skills are enabled
113
152
  if self.skills:
114
153
  self._setup_skills_configuration()
@@ -117,11 +156,15 @@ class Claude(Model):
117
156
  client_params: Dict[str, Any] = {}
118
157
 
119
158
  self.api_key = self.api_key or getenv("ANTHROPIC_API_KEY")
120
- if not self.api_key:
121
- log_error("ANTHROPIC_API_KEY not set. Please set the ANTHROPIC_API_KEY environment variable.")
159
+ self.auth_token = self.auth_token or getenv("ANTHROPIC_AUTH_TOKEN")
160
+ if not (self.api_key or self.auth_token):
161
+ log_error(
162
+ "ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN not set. Please set the ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable."
163
+ )
122
164
 
123
165
  # Add API key to client parameters
124
166
  client_params["api_key"] = self.api_key
167
+ client_params["auth_token"] = self.auth_token
125
168
  if self.timeout is not None:
126
169
  client_params["timeout"] = self.timeout
127
170
 
@@ -132,36 +175,63 @@ class Claude(Model):
132
175
  client_params["default_headers"] = self.default_headers
133
176
  return client_params
134
177
 
135
- def _has_beta_features(self) -> bool:
136
- """Check if the model has any Anthropic beta features enabled."""
137
- return (
138
- self.mcp_servers is not None
139
- or self.context_management is not None
140
- or self.skills is not None
141
- or self.betas is not None
142
- )
178
+ def _supports_structured_outputs(self) -> bool:
179
+ """
180
+ Check if the current model supports native structured outputs.
143
181
 
144
- def get_client(self) -> AnthropicClient:
182
+ Returns:
183
+ bool: True if model supports structured outputs
145
184
  """
146
- Returns an instance of the Anthropic client.
185
+ # If model is in blacklist, it doesn't support structured outputs
186
+ if self.id in self.NON_STRUCTURED_OUTPUT_MODELS:
187
+ return False
188
+
189
+ # Check for legacy model patterns which don't support structured outputs
190
+ if self.id.startswith("claude-3-"):
191
+ return False
192
+ if self.id.startswith("claude-sonnet-4-") and not self.id.startswith("claude-sonnet-4-5"):
193
+ return False
194
+ if self.id.startswith("claude-opus-4-") and not (
195
+ self.id.startswith("claude-opus-4-1") or self.id.startswith("claude-opus-4-5")
196
+ ):
197
+ return False
198
+
199
+ return True
200
+
201
+ def _using_structured_outputs(
202
+ self,
203
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
204
+ tools: Optional[List[Dict[str, Any]]] = None,
205
+ ) -> bool:
147
206
  """
148
- if self.client and not self.client.is_closed():
149
- return self.client
207
+ Check if structured outputs are being used in this request.
150
208
 
151
- _client_params = self._get_client_params()
152
- self.client = AnthropicClient(**_client_params)
153
- return self.client
209
+ Args:
210
+ response_format: Response format parameter
211
+ tools: Tools list to check for strict mode
154
212
 
155
- def get_async_client(self) -> AsyncAnthropicClient:
156
- """
157
- Returns an instance of the async Anthropic client.
213
+ Returns:
214
+ bool: True if structured outputs are in use
158
215
  """
159
- if self.async_client and not self.async_client.is_closed():
160
- return self.async_client
216
+ # Check for output_format usage
217
+ if response_format is not None:
218
+ if self._supports_structured_outputs():
219
+ return True
220
+ else:
221
+ log_warning(
222
+ f"Model '{self.id}' does not support structured outputs. "
223
+ "Structured output features will not be available for this model."
224
+ )
161
225
 
162
- _client_params = self._get_client_params()
163
- self.async_client = AsyncAnthropicClient(**_client_params)
164
- return self.async_client
226
+ # Check for strict tools
227
+ if tools:
228
+ for tool in tools:
229
+ if tool.get("type") == "function":
230
+ func_def = tool.get("function", {})
231
+ if func_def.get("strict") is True:
232
+ return True
233
+
234
+ return False
165
235
 
166
236
  def _validate_thinking_support(self) -> None:
167
237
  """
@@ -199,7 +269,214 @@ class Claude(Model):
199
269
  if beta not in self.betas:
200
270
  self.betas.append(beta)
201
271
 
202
- def get_request_params(self) -> Dict[str, Any]:
272
+ def _ensure_additional_properties_false(self, schema: Dict[str, Any]) -> None:
273
+ """
274
+ Recursively ensure all object types have additionalProperties: false.
275
+ """
276
+ if isinstance(schema, dict):
277
+ if schema.get("type") == "object":
278
+ schema["additionalProperties"] = False
279
+
280
+ # Recursively process nested schemas
281
+ for key, value in schema.items():
282
+ if key in ["properties", "items", "allOf", "anyOf", "oneOf"]:
283
+ if isinstance(value, dict):
284
+ self._ensure_additional_properties_false(value)
285
+ elif isinstance(value, list):
286
+ for item in value:
287
+ if isinstance(item, dict):
288
+ self._ensure_additional_properties_false(item)
289
+
290
+ def _build_output_format(self, response_format: Optional[Union[Dict, Type[BaseModel]]]) -> Optional[Dict[str, Any]]:
291
+ """
292
+ Build Anthropic output_format parameter from response_format.
293
+
294
+ Args:
295
+ response_format: Pydantic model or dict format
296
+
297
+ Returns:
298
+ Dict with output_format structure or None
299
+ """
300
+ if response_format is None:
301
+ return None
302
+
303
+ if not self._supports_structured_outputs():
304
+ return None
305
+
306
+ # Handle Pydantic BaseModel
307
+ if isinstance(response_format, type) and issubclass(response_format, BaseModel):
308
+ try:
309
+ # Try to use Anthropic SDK's transform_schema helper if available
310
+ from anthropic import transform_schema
311
+
312
+ schema = transform_schema(response_format.model_json_schema())
313
+ except (ImportError, AttributeError):
314
+ # Fallback to direct schema conversion
315
+ schema = response_format.model_json_schema()
316
+ # Ensure additionalProperties is False
317
+ if isinstance(schema, dict):
318
+ if "additionalProperties" not in schema:
319
+ schema["additionalProperties"] = False
320
+ # Recursively ensure all object types have additionalProperties: false
321
+ self._ensure_additional_properties_false(schema)
322
+
323
+ return {"type": "json_schema", "schema": schema}
324
+
325
+ # Handle dict format
326
+ elif isinstance(response_format, dict):
327
+ # Claude only supports json_schema, not json_object
328
+ if response_format.get("type") == "json_object":
329
+ return None
330
+ return response_format
331
+
332
+ return None
333
+
334
+ def _validate_structured_outputs_usage(
335
+ self,
336
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
337
+ tools: Optional[List[Dict[str, Any]]] = None,
338
+ ) -> None:
339
+ """
340
+ Validate that structured outputs are only used with supported models.
341
+
342
+ Raises:
343
+ ValueError: If structured outputs are used with unsupported model
344
+ """
345
+ if not self._using_structured_outputs(response_format, tools):
346
+ return
347
+
348
+ if not self._supports_structured_outputs():
349
+ raise ValueError(f"Model '{self.id}' does not support structured outputs.\n\n")
350
+
351
+ def _has_beta_features(
352
+ self,
353
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
354
+ tools: Optional[List[Dict[str, Any]]] = None,
355
+ ) -> bool:
356
+ """Check if the model has any Anthropic beta features enabled."""
357
+ return (
358
+ self.mcp_servers is not None
359
+ or self.context_management is not None
360
+ or self.skills is not None
361
+ or self.betas is not None
362
+ or self._using_structured_outputs(response_format, tools)
363
+ )
364
+
365
+ def get_client(self) -> AnthropicClient:
366
+ """
367
+ Returns an instance of the Anthropic client.
368
+ """
369
+ if self.client and not self.client.is_closed():
370
+ return self.client
371
+
372
+ _client_params = self._get_client_params()
373
+ if self.http_client:
374
+ if isinstance(self.http_client, httpx.Client):
375
+ _client_params["http_client"] = self.http_client
376
+ else:
377
+ log_warning("http_client is not an instance of httpx.Client. Using default global httpx.Client.")
378
+ # Use global sync client when user http_client is invalid
379
+ _client_params["http_client"] = get_default_sync_client()
380
+ else:
381
+ # Use global sync client when no custom http_client is provided
382
+ _client_params["http_client"] = get_default_sync_client()
383
+ self.client = AnthropicClient(**_client_params)
384
+ return self.client
385
+
386
+ def get_async_client(self) -> AsyncAnthropicClient:
387
+ """
388
+ Returns an instance of the async Anthropic client.
389
+ """
390
+ if self.async_client and not self.async_client.is_closed():
391
+ return self.async_client
392
+
393
+ _client_params = self._get_client_params()
394
+ if self.http_client:
395
+ if isinstance(self.http_client, httpx.AsyncClient):
396
+ _client_params["http_client"] = self.http_client
397
+ else:
398
+ log_warning(
399
+ "http_client is not an instance of httpx.AsyncClient. Using default global httpx.AsyncClient."
400
+ )
401
+ # Use global async client when user http_client is invalid
402
+ _client_params["http_client"] = get_default_async_client()
403
+ else:
404
+ # Use global async client when no custom http_client is provided
405
+ _client_params["http_client"] = get_default_async_client()
406
+ self.async_client = AsyncAnthropicClient(**_client_params)
407
+ return self.async_client
408
+
409
+ def to_dict(self) -> Dict[str, Any]:
410
+ """
411
+ Convert the model to a dictionary.
412
+
413
+ Returns:
414
+ Dict[str, Any]: The dictionary representation of the model.
415
+ """
416
+ model_dict = super().to_dict()
417
+ model_dict.update(
418
+ {
419
+ "max_tokens": self.max_tokens,
420
+ "thinking": self.thinking,
421
+ "temperature": self.temperature,
422
+ "stop_sequences": self.stop_sequences,
423
+ "top_p": self.top_p,
424
+ "top_k": self.top_k,
425
+ "cache_system_prompt": self.cache_system_prompt,
426
+ "extended_cache_time": self.extended_cache_time,
427
+ "betas": self.betas,
428
+ }
429
+ )
430
+ cleaned_dict = {k: v for k, v in model_dict.items() if v is not None}
431
+ return cleaned_dict
432
+
433
+ def count_tokens(
434
+ self,
435
+ messages: List[Message],
436
+ tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
437
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
438
+ ) -> int:
439
+ anthropic_messages, system_prompt = format_messages(messages, compress_tool_results=True)
440
+ anthropic_tools = None
441
+ if tools:
442
+ formatted_tools = self._format_tools(tools)
443
+ anthropic_tools = format_tools_for_model(formatted_tools)
444
+
445
+ kwargs: Dict[str, Any] = {"messages": anthropic_messages, "model": self.id}
446
+ if system_prompt:
447
+ kwargs["system"] = system_prompt
448
+ if anthropic_tools:
449
+ kwargs["tools"] = anthropic_tools
450
+
451
+ response = self.get_client().messages.count_tokens(**kwargs)
452
+ return response.input_tokens + count_schema_tokens(response_format, self.id)
453
+
454
+ async def acount_tokens(
455
+ self,
456
+ messages: List[Message],
457
+ tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
458
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
459
+ ) -> int:
460
+ anthropic_messages, system_prompt = format_messages(messages, compress_tool_results=True)
461
+ anthropic_tools = None
462
+ if tools:
463
+ formatted_tools = self._format_tools(tools)
464
+ anthropic_tools = format_tools_for_model(formatted_tools)
465
+
466
+ kwargs: Dict[str, Any] = {"messages": anthropic_messages, "model": self.id}
467
+ if system_prompt:
468
+ kwargs["system"] = system_prompt
469
+ if anthropic_tools:
470
+ kwargs["tools"] = anthropic_tools
471
+
472
+ response = await self.get_async_client().messages.count_tokens(**kwargs)
473
+ return response.input_tokens + count_schema_tokens(response_format, self.id)
474
+
475
+ def get_request_params(
476
+ self,
477
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
478
+ tools: Optional[List[Dict[str, Any]]] = None,
479
+ ) -> Dict[str, Any]:
203
480
  """
204
481
  Generate keyword arguments for API requests.
205
482
  """
@@ -220,8 +497,20 @@ class Claude(Model):
220
497
  _request_params["top_p"] = self.top_p
221
498
  if self.top_k:
222
499
  _request_params["top_k"] = self.top_k
223
- if self.betas:
224
- _request_params["betas"] = self.betas
500
+
501
+ # Build betas list - include existing betas and add new one if needed
502
+ betas_list = list(self.betas) if self.betas else []
503
+
504
+ # Add structured outputs beta header if using structured outputs
505
+ if self._using_structured_outputs(response_format, tools):
506
+ beta_header = "structured-outputs-2025-11-13"
507
+ if beta_header not in betas_list:
508
+ betas_list.append(beta_header)
509
+
510
+ # Include betas if any are present
511
+ if betas_list:
512
+ _request_params["betas"] = betas_list
513
+
225
514
  if self.context_management:
226
515
  _request_params["context_management"] = self.context_management
227
516
  if self.mcp_servers:
@@ -229,7 +518,6 @@ class Claude(Model):
229
518
  {k: v for k, v in asdict(server).items() if v is not None} for server in self.mcp_servers
230
519
  ]
231
520
  if self.skills:
232
- _request_params["betas"] = self.betas
233
521
  _request_params["container"] = {"skills": self.skills}
234
522
  if self.request_params:
235
523
  _request_params.update(self.request_params)
@@ -237,18 +525,27 @@ class Claude(Model):
237
525
  return _request_params
238
526
 
239
527
  def _prepare_request_kwargs(
240
- self, system_message: str, tools: Optional[List[Dict[str, Any]]] = None
528
+ self,
529
+ system_message: str,
530
+ tools: Optional[List[Dict[str, Any]]] = None,
531
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
241
532
  ) -> Dict[str, Any]:
242
533
  """
243
534
  Prepare the request keyword arguments for the API call.
244
535
 
245
536
  Args:
246
537
  system_message (str): The concatenated system messages.
538
+ tools: Optional list of tools
539
+ response_format: Optional response format (Pydantic model or dict)
247
540
 
248
541
  Returns:
249
542
  Dict[str, Any]: The request keyword arguments.
250
543
  """
251
- request_kwargs = self.get_request_params().copy()
544
+ # Validate structured outputs usage
545
+ self._validate_structured_outputs_usage(response_format, tools)
546
+
547
+ # Pass response_format and tools to get_request_params for beta header handling
548
+ request_kwargs = self.get_request_params(response_format=response_format, tools=tools).copy()
252
549
  if system_message:
253
550
  if self.cache_system_prompt:
254
551
  cache_control = (
@@ -269,9 +566,15 @@ class Claude(Model):
269
566
  else:
270
567
  tools = [code_execution_tool]
271
568
 
569
+ # Format tools (this will handle strict mode)
272
570
  if tools:
273
571
  request_kwargs["tools"] = format_tools_for_model(tools)
274
572
 
573
+ # Build output_format if response_format is provided
574
+ output_format = self._build_output_format(response_format)
575
+ if output_format:
576
+ request_kwargs["output_format"] = output_format
577
+
275
578
  if request_kwargs:
276
579
  log_debug(f"Calling {self.provider} with request parameters: {request_kwargs}", log_level=2)
277
580
  return request_kwargs
@@ -284,6 +587,7 @@ class Claude(Model):
284
587
  tools: Optional[List[Dict[str, Any]]] = None,
285
588
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
286
589
  run_response: Optional[RunOutput] = None,
590
+ compress_tool_results: bool = False,
287
591
  ) -> ModelResponse:
288
592
  """
289
593
  Send a request to the Anthropic API to generate a response.
@@ -292,10 +596,10 @@ class Claude(Model):
292
596
  if run_response and run_response.metrics:
293
597
  run_response.metrics.set_time_to_first_token()
294
598
 
295
- chat_messages, system_message = format_messages(messages)
296
- request_kwargs = self._prepare_request_kwargs(system_message, tools)
599
+ chat_messages, system_message = format_messages(messages, compress_tool_results=compress_tool_results)
600
+ request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
297
601
 
298
- if self._has_beta_features():
602
+ if self._has_beta_features(response_format=response_format, tools=tools):
299
603
  assistant_message.metrics.start_timer()
300
604
  provider_response = self.get_client().beta.messages.create(
301
605
  model=self.id,
@@ -340,6 +644,7 @@ class Claude(Model):
340
644
  tools: Optional[List[Dict[str, Any]]] = None,
341
645
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
342
646
  run_response: Optional[RunOutput] = None,
647
+ compress_tool_results: bool = False,
343
648
  ) -> Any:
344
649
  """
345
650
  Stream a response from the Anthropic API.
@@ -355,15 +660,15 @@ class Claude(Model):
355
660
  RateLimitError: If the API rate limit is exceeded
356
661
  APIStatusError: For other API-related errors
357
662
  """
358
- chat_messages, system_message = format_messages(messages)
359
- request_kwargs = self._prepare_request_kwargs(system_message, tools)
663
+ chat_messages, system_message = format_messages(messages, compress_tool_results=compress_tool_results)
664
+ request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
360
665
 
361
666
  try:
362
667
  if run_response and run_response.metrics:
363
668
  run_response.metrics.set_time_to_first_token()
364
669
 
365
670
  # Beta features
366
- if self._has_beta_features():
671
+ if self._has_beta_features(response_format=response_format, tools=tools):
367
672
  assistant_message.metrics.start_timer()
368
673
  with self.get_client().beta.messages.stream(
369
674
  model=self.id,
@@ -371,7 +676,7 @@ class Claude(Model):
371
676
  **request_kwargs,
372
677
  ) as stream:
373
678
  for chunk in stream:
374
- yield self._parse_provider_response_delta(chunk) # type: ignore
679
+ yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
375
680
  else:
376
681
  assistant_message.metrics.start_timer()
377
682
  with self.get_client().messages.stream(
@@ -380,7 +685,7 @@ class Claude(Model):
380
685
  **request_kwargs,
381
686
  ) as stream:
382
687
  for chunk in stream: # type: ignore
383
- yield self._parse_provider_response_delta(chunk) # type: ignore
688
+ yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
384
689
 
385
690
  assistant_message.metrics.stop_timer()
386
691
 
@@ -407,6 +712,7 @@ class Claude(Model):
407
712
  tools: Optional[List[Dict[str, Any]]] = None,
408
713
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
409
714
  run_response: Optional[RunOutput] = None,
715
+ compress_tool_results: bool = False,
410
716
  ) -> ModelResponse:
411
717
  """
412
718
  Send an asynchronous request to the Anthropic API to generate a response.
@@ -415,11 +721,11 @@ class Claude(Model):
415
721
  if run_response and run_response.metrics:
416
722
  run_response.metrics.set_time_to_first_token()
417
723
 
418
- chat_messages, system_message = format_messages(messages)
419
- request_kwargs = self._prepare_request_kwargs(system_message, tools)
724
+ chat_messages, system_message = format_messages(messages, compress_tool_results=compress_tool_results)
725
+ request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
420
726
 
421
727
  # Beta features
422
- if self._has_beta_features():
728
+ if self._has_beta_features(response_format=response_format, tools=tools):
423
729
  assistant_message.metrics.start_timer()
424
730
  provider_response = await self.get_async_client().beta.messages.create(
425
731
  model=self.id,
@@ -464,6 +770,7 @@ class Claude(Model):
464
770
  tools: Optional[List[Dict[str, Any]]] = None,
465
771
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
466
772
  run_response: Optional[RunOutput] = None,
773
+ compress_tool_results: bool = False,
467
774
  ) -> AsyncIterator[ModelResponse]:
468
775
  """
469
776
  Stream an asynchronous response from the Anthropic API.
@@ -480,10 +787,10 @@ class Claude(Model):
480
787
  if run_response and run_response.metrics:
481
788
  run_response.metrics.set_time_to_first_token()
482
789
 
483
- chat_messages, system_message = format_messages(messages)
484
- request_kwargs = self._prepare_request_kwargs(system_message, tools)
790
+ chat_messages, system_message = format_messages(messages, compress_tool_results=compress_tool_results)
791
+ request_kwargs = self._prepare_request_kwargs(system_message, tools=tools, response_format=response_format)
485
792
 
486
- if self._has_beta_features():
793
+ if self._has_beta_features(response_format=response_format, tools=tools):
487
794
  assistant_message.metrics.start_timer()
488
795
  async with self.get_async_client().beta.messages.stream(
489
796
  model=self.id,
@@ -491,7 +798,7 @@ class Claude(Model):
491
798
  **request_kwargs,
492
799
  ) as stream:
493
800
  async for chunk in stream:
494
- yield self._parse_provider_response_delta(chunk) # type: ignore
801
+ yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
495
802
  else:
496
803
  assistant_message.metrics.start_timer()
497
804
  async with self.get_async_client().messages.stream(
@@ -500,7 +807,7 @@ class Claude(Model):
500
807
  **request_kwargs,
501
808
  ) as stream:
502
809
  async for chunk in stream: # type: ignore
503
- yield self._parse_provider_response_delta(chunk) # type: ignore
810
+ yield self._parse_provider_response_delta(chunk, response_format=response_format) # type: ignore
504
811
 
505
812
  assistant_message.metrics.stop_timer()
506
813
 
@@ -525,12 +832,18 @@ class Claude(Model):
525
832
  return tool_call_prompt
526
833
  return None
527
834
 
528
- def _parse_provider_response(self, response: Union[AnthropicMessage, BetaMessage], **kwargs) -> ModelResponse:
835
+ def _parse_provider_response(
836
+ self,
837
+ response: Union[AnthropicMessage, BetaMessage],
838
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
839
+ **kwargs,
840
+ ) -> ModelResponse:
529
841
  """
530
842
  Parse the Claude response into a ModelResponse.
531
843
 
532
844
  Args:
533
845
  response: Raw response from Anthropic
846
+ response_format: Optional response format for structured output parsing
534
847
 
535
848
  Returns:
536
849
  ModelResponse: Parsed response data
@@ -543,10 +856,32 @@ class Claude(Model):
543
856
  if response.content:
544
857
  for block in response.content:
545
858
  if block.type == "text":
859
+ text_content = block.text
860
+
546
861
  if model_response.content is None:
547
- model_response.content = block.text
862
+ model_response.content = text_content
548
863
  else:
549
- model_response.content += block.text
864
+ model_response.content += text_content
865
+
866
+ # Handle structured outputs (JSON outputs)
867
+ if (
868
+ response_format is not None
869
+ and isinstance(response_format, type)
870
+ and issubclass(response_format, BaseModel)
871
+ ):
872
+ if text_content:
873
+ try:
874
+ # Parse JSON from text content
875
+ parsed_data = json.loads(text_content)
876
+ # Validate against Pydantic model
877
+ model_response.parsed = response_format.model_validate(parsed_data)
878
+ log_debug(f"Successfully parsed structured output: {model_response.parsed}")
879
+ except json.JSONDecodeError as e:
880
+ log_warning(f"Failed to parse JSON from structured output: {e}")
881
+ except ValidationError as e:
882
+ log_warning(f"Failed to validate structured output against schema: {e}")
883
+ except Exception as e:
884
+ log_warning(f"Unexpected error parsing structured output: {e}")
550
885
 
551
886
  # Capture citations from the response
552
887
  if block.citations is not None:
@@ -634,24 +969,29 @@ class Claude(Model):
634
969
  ContentBlockStopEvent,
635
970
  MessageStopEvent,
636
971
  BetaRawContentBlockDeltaEvent,
972
+ BetaRawContentBlockStartEvent,
973
+ ParsedBetaContentBlockStopEvent,
974
+ ParsedBetaMessageStopEvent,
637
975
  ],
976
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
638
977
  ) -> ModelResponse:
639
978
  """
640
979
  Parse the Claude streaming response into ModelProviderResponse objects.
641
980
 
642
981
  Args:
643
982
  response: Raw response chunk from Anthropic
983
+ response_format: Optional response format for structured output parsing
644
984
 
645
985
  Returns:
646
986
  ModelResponse: Iterator of parsed response data
647
987
  """
648
988
  model_response = ModelResponse()
649
989
 
650
- if isinstance(response, ContentBlockStartEvent):
990
+ if isinstance(response, (ContentBlockStartEvent, BetaRawContentBlockStartEvent)):
651
991
  if response.content_block.type == "redacted_reasoning_content":
652
992
  model_response.redacted_reasoning_content = response.content_block.data
653
993
 
654
- if isinstance(response, ContentBlockDeltaEvent):
994
+ if isinstance(response, (ContentBlockDeltaEvent, BetaRawContentBlockDeltaEvent)):
655
995
  # Handle text content
656
996
  if response.delta.type == "text_delta":
657
997
  model_response.content = response.delta.text
@@ -663,11 +1003,11 @@ class Claude(Model):
663
1003
  "signature": response.delta.signature,
664
1004
  }
665
1005
 
666
- elif isinstance(response, ContentBlockStopEvent):
1006
+ elif isinstance(response, (ContentBlockStopEvent, ParsedBetaContentBlockStopEvent)):
667
1007
  if response.content_block.type == "tool_use": # type: ignore
668
1008
  tool_use = response.content_block # type: ignore
669
- tool_name = tool_use.name
670
- tool_input = tool_use.input
1009
+ tool_name = tool_use.name # type: ignore
1010
+ tool_input = tool_use.input # type: ignore
671
1011
 
672
1012
  function_def = {"name": tool_name}
673
1013
  if tool_input:
@@ -677,17 +1017,30 @@ class Claude(Model):
677
1017
 
678
1018
  model_response.tool_calls = [
679
1019
  {
680
- "id": tool_use.id,
1020
+ "id": tool_use.id, # type: ignore
681
1021
  "type": "function",
682
1022
  "function": function_def,
683
1023
  }
684
1024
  ]
685
1025
 
686
- # Capture citations from the final response
687
- elif isinstance(response, MessageStopEvent):
1026
+ # Capture citations from the final response and handle structured outputs
1027
+ elif isinstance(response, (MessageStopEvent, ParsedBetaMessageStopEvent)):
1028
+ # In streaming mode, content has already been emitted via ContentBlockDeltaEvent chunks
1029
+ # Setting content here would cause duplication since _populate_stream_data accumulates with +=
1030
+ # Keep content empty to avoid duplication
688
1031
  model_response.content = ""
689
1032
  model_response.citations = Citations(raw=[], urls=[], documents=[])
1033
+
1034
+ # Accumulate text content for structured output parsing (but don't set model_response.content)
1035
+ # The text was already streamed via ContentBlockDeltaEvent chunks
1036
+ accumulated_text = ""
1037
+
690
1038
  for block in response.message.content: # type: ignore
1039
+ # Handle text blocks for structured output parsing
1040
+ if block.type == "text":
1041
+ accumulated_text += block.text # type: ignore
1042
+
1043
+ # Handle citations
691
1044
  citations = getattr(block, "citations", None)
692
1045
  if not citations:
693
1046
  continue
@@ -702,6 +1055,28 @@ class Claude(Model):
702
1055
  DocumentCitation(document_title=citation.document_title, cited_text=citation.cited_text)
703
1056
  )
704
1057
 
1058
+ # Handle structured outputs (JSON outputs) from accumulated text
1059
+ # Note: We parse from accumulated_text but don't set model_response.content to avoid duplication
1060
+ # The content was already streamed via ContentBlockDeltaEvent chunks
1061
+ if (
1062
+ response_format is not None
1063
+ and isinstance(response_format, type)
1064
+ and issubclass(response_format, BaseModel)
1065
+ ):
1066
+ if accumulated_text:
1067
+ try:
1068
+ # Parse JSON from accumulated text content
1069
+ parsed_data = json.loads(accumulated_text)
1070
+ # Validate against Pydantic model
1071
+ model_response.parsed = response_format.model_validate(parsed_data)
1072
+ log_debug(f"Successfully parsed structured output from stream: {model_response.parsed}")
1073
+ except json.JSONDecodeError as e:
1074
+ log_warning(f"Failed to parse JSON from structured output in stream: {e}")
1075
+ except ValidationError as e:
1076
+ log_warning(f"Failed to validate structured output against schema in stream: {e}")
1077
+ except Exception as e:
1078
+ log_warning(f"Unexpected error parsing structured output in stream: {e}")
1079
+
705
1080
  # Capture context management information if present
706
1081
  if self.context_management is not None and hasattr(response.message, "context_management"): # type: ignore
707
1082
  context_mgmt = response.message.context_management # type: ignore