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
@@ -6,16 +6,19 @@ import httpx
6
6
  from pydantic import BaseModel
7
7
  from typing_extensions import Literal
8
8
 
9
- from agno.exceptions import ModelProviderError
9
+ from agno.exceptions import ModelAuthenticationError, ModelProviderError
10
10
  from agno.media import File
11
11
  from agno.models.base import Model
12
12
  from agno.models.message import Citations, Message, UrlCitation
13
13
  from agno.models.metrics import Metrics
14
14
  from agno.models.response import ModelResponse
15
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
16
18
  from agno.utils.log import log_debug, log_error, log_warning
17
19
  from agno.utils.models.openai_responses import images_to_message
18
20
  from agno.utils.models.schema_utils import get_response_schema_for_provider
21
+ from agno.utils.tokens import count_schema_tokens
19
22
 
20
23
  try:
21
24
  from openai import APIConnectionError, APIStatusError, AsyncOpenAI, OpenAI, RateLimitError
@@ -116,7 +119,10 @@ class OpenAIResponses(Model):
116
119
  if not self.api_key:
117
120
  self.api_key = getenv("OPENAI_API_KEY")
118
121
  if not self.api_key:
119
- log_error("OPENAI_API_KEY not set. Please set the OPENAI_API_KEY environment variable.")
122
+ raise ModelAuthenticationError(
123
+ message="OPENAI_API_KEY not set. Please set the OPENAI_API_KEY environment variable.",
124
+ model_name=self.name,
125
+ )
120
126
 
121
127
  # Define base client params
122
128
  base_params = {
@@ -140,7 +146,7 @@ class OpenAIResponses(Model):
140
146
 
141
147
  def get_client(self) -> OpenAI:
142
148
  """
143
- Returns an OpenAI client.
149
+ Returns an OpenAI client. Caches the client to avoid recreating it on every request.
144
150
 
145
151
  Returns:
146
152
  OpenAI: An instance of the OpenAI client.
@@ -149,18 +155,18 @@ class OpenAIResponses(Model):
149
155
  return self.client
150
156
 
151
157
  client_params: Dict[str, Any] = self._get_client_params()
152
- if self.http_client:
153
- if isinstance(self.http_client, httpx.Client):
154
- client_params["http_client"] = self.http_client
155
- else:
156
- log_debug("http_client is not an instance of httpx.Client.")
158
+ if self.http_client is not None:
159
+ client_params["http_client"] = self.http_client
160
+ else:
161
+ # Use global sync client when no custom http_client is provided
162
+ client_params["http_client"] = get_default_sync_client()
157
163
 
158
164
  self.client = OpenAI(**client_params)
159
165
  return self.client
160
166
 
161
167
  def get_async_client(self) -> AsyncOpenAI:
162
168
  """
163
- Returns an asynchronous OpenAI client.
169
+ Returns an asynchronous OpenAI client. Caches the client to avoid recreating it on every request.
164
170
 
165
171
  Returns:
166
172
  AsyncOpenAI: An instance of the asynchronous OpenAI client.
@@ -172,12 +178,8 @@ class OpenAIResponses(Model):
172
178
  if self.http_client and isinstance(self.http_client, httpx.AsyncClient):
173
179
  client_params["http_client"] = self.http_client
174
180
  else:
175
- if self.http_client:
176
- log_debug("The current http_client is not async. A default httpx.AsyncClient will be used instead.")
177
- # Create a new async HTTP client with custom limits
178
- client_params["http_client"] = httpx.AsyncClient(
179
- limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100)
180
- )
181
+ # Use global async client when no custom http_client is provided
182
+ client_params["http_client"] = get_default_async_client()
181
183
 
182
184
  self.async_client = AsyncOpenAI(**client_params)
183
185
  return self.async_client
@@ -233,8 +235,8 @@ class OpenAIResponses(Model):
233
235
  "strict": self.strict_output,
234
236
  }
235
237
  else:
236
- # JSON mode
237
- text_params["format"] = {"type": "json_object"}
238
+ # Pass through directly, user handles everything
239
+ text_params["format"] = response_format
238
240
 
239
241
  # Add text parameter if there are any text-level params
240
242
  if text_params:
@@ -307,6 +309,8 @@ class OpenAIResponses(Model):
307
309
 
308
310
  def _upload_file(self, file: File) -> Optional[str]:
309
311
  """Upload a file to the OpenAI vector database."""
312
+ from pathlib import Path
313
+ from urllib.parse import urlparse
310
314
 
311
315
  if file.url is not None:
312
316
  file_content_tuple = file.file_url_content
@@ -314,13 +318,12 @@ class OpenAIResponses(Model):
314
318
  file_content = file_content_tuple[0]
315
319
  else:
316
320
  return None
317
- file_name = file.url.split("/")[-1]
321
+ file_name = Path(urlparse(file.url).path).name or "file"
318
322
  file_tuple = (file_name, file_content)
319
323
  result = self.get_client().files.create(file=file_tuple, purpose="assistants")
320
324
  return result.id
321
325
  elif file.filepath is not None:
322
326
  import mimetypes
323
- from pathlib import Path
324
327
 
325
328
  file_path = file.filepath if isinstance(file.filepath, Path) else Path(file.filepath)
326
329
  if file_path.exists() and file_path.is_file():
@@ -362,19 +365,25 @@ class OpenAIResponses(Model):
362
365
  return vector_store.id
363
366
 
364
367
  def _format_tool_params(
365
- self, messages: List[Message], tools: Optional[List[Dict[str, Any]]] = None
368
+ self, messages: List[Message], tools: Optional[List[Union[Function, Dict[str, Any]]]] = None
366
369
  ) -> List[Dict[str, Any]]:
367
370
  """Format the tool parameters for the OpenAI Responses API."""
368
371
  formatted_tools = []
369
372
  if tools:
370
373
  for _tool in tools:
371
- if _tool.get("type") == "function":
374
+ if isinstance(_tool, Function):
375
+ _tool_dict = _tool.to_dict()
376
+ _tool_dict["type"] = "function"
377
+ for prop in _tool_dict.get("parameters", {}).get("properties", {}).values():
378
+ if isinstance(prop.get("type", ""), list):
379
+ prop["type"] = prop["type"][0]
380
+ formatted_tools.append(_tool_dict)
381
+ elif _tool.get("type") == "function":
372
382
  _tool_dict = _tool.get("function", {})
373
383
  _tool_dict["type"] = "function"
374
384
  for prop in _tool_dict.get("parameters", {}).get("properties", {}).values():
375
385
  if isinstance(prop.get("type", ""), list):
376
386
  prop["type"] = prop["type"][0]
377
-
378
387
  formatted_tools.append(_tool_dict)
379
388
  else:
380
389
  formatted_tools.append(_tool)
@@ -393,17 +402,20 @@ class OpenAIResponses(Model):
393
402
 
394
403
  # Add the file IDs to the tool parameters
395
404
  for _tool in formatted_tools:
396
- if _tool["type"] == "file_search" and vector_store_id is not None:
405
+ if _tool.get("type", "") == "file_search" and vector_store_id is not None:
397
406
  _tool["vector_store_ids"] = [vector_store_id]
398
407
 
399
408
  return formatted_tools
400
409
 
401
- def _format_messages(self, messages: List[Message]) -> List[Union[Dict[str, Any], ResponseReasoningItem]]:
410
+ def _format_messages(
411
+ self, messages: List[Message], compress_tool_results: bool = False
412
+ ) -> List[Union[Dict[str, Any], ResponseReasoningItem]]:
402
413
  """
403
414
  Format a message into the format expected by OpenAI.
404
415
 
405
416
  Args:
406
417
  messages (List[Message]): The message to format.
418
+ compress_tool_results: Whether to compress tool results.
407
419
 
408
420
  Returns:
409
421
  Dict[str, Any]: The formatted message.
@@ -448,7 +460,7 @@ class OpenAIResponses(Model):
448
460
  if message.role in ["user", "system"]:
449
461
  message_dict: Dict[str, Any] = {
450
462
  "role": self.role_map[message.role],
451
- "content": message.content,
463
+ "content": message.get_content(use_compressed_content=compress_tool_results),
452
464
  }
453
465
  message_dict = {k: v for k, v in message_dict.items() if v is not None}
454
466
 
@@ -472,7 +484,9 @@ class OpenAIResponses(Model):
472
484
 
473
485
  # Tool call result
474
486
  elif message.role == "tool":
475
- if message.tool_call_id and message.content is not None:
487
+ tool_result = message.get_content(use_compressed_content=compress_tool_results)
488
+
489
+ if message.tool_call_id and tool_result is not None:
476
490
  function_call_id = message.tool_call_id
477
491
  # Normalize: if a fc_* id was provided, translate to its corresponding call_* id
478
492
  if isinstance(function_call_id, str) and function_call_id in fc_id_to_call_id:
@@ -480,7 +494,7 @@ class OpenAIResponses(Model):
480
494
  else:
481
495
  call_id_value = function_call_id
482
496
  formatted_messages.append(
483
- {"type": "function_call_output", "call_id": call_id_value, "output": message.content}
497
+ {"type": "function_call_output", "call_id": call_id_value, "output": tool_result}
484
498
  )
485
499
  # Tool Calls
486
500
  elif message.tool_calls is not None and len(message.tool_calls) > 0:
@@ -514,6 +528,49 @@ class OpenAIResponses(Model):
514
528
  formatted_messages.append(reasoning_output)
515
529
  return formatted_messages
516
530
 
531
+ def count_tokens(
532
+ self,
533
+ messages: List[Message],
534
+ tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
535
+ output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
536
+ ) -> int:
537
+ try:
538
+ formatted_input = self._format_messages(messages, compress_tool_results=True)
539
+ formatted_tools = self._format_tool_params(messages, tools) if tools is not None else None
540
+
541
+ response = self.get_client().responses.input_tokens.count(
542
+ model=self.id,
543
+ input=formatted_input, # type: ignore
544
+ instructions=self.instructions, # type: ignore
545
+ tools=formatted_tools, # type: ignore
546
+ )
547
+ return response.input_tokens + count_schema_tokens(output_schema, self.id)
548
+ except Exception as e:
549
+ log_warning(f"Failed to count tokens via API: {e}")
550
+ return super().count_tokens(messages, tools, output_schema)
551
+
552
+ async def acount_tokens(
553
+ self,
554
+ messages: List[Message],
555
+ tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
556
+ output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
557
+ ) -> int:
558
+ """Async version of count_tokens using the async client."""
559
+ try:
560
+ formatted_input = self._format_messages(messages, compress_tool_results=True)
561
+ formatted_tools = self._format_tool_params(messages, tools) if tools else None
562
+
563
+ response = await self.get_async_client().responses.input_tokens.count(
564
+ model=self.id,
565
+ input=formatted_input, # type: ignore
566
+ instructions=self.instructions, # type: ignore
567
+ tools=formatted_tools, # type: ignore
568
+ )
569
+ return response.input_tokens + count_schema_tokens(output_schema, self.id)
570
+ except Exception as e:
571
+ log_warning(f"Failed to count tokens via API: {e}")
572
+ return await super().acount_tokens(messages, tools, output_schema)
573
+
517
574
  def invoke(
518
575
  self,
519
576
  messages: List[Message],
@@ -522,6 +579,7 @@ class OpenAIResponses(Model):
522
579
  tools: Optional[List[Dict[str, Any]]] = None,
523
580
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
524
581
  run_response: Optional[RunOutput] = None,
582
+ compress_tool_results: bool = False,
525
583
  ) -> ModelResponse:
526
584
  """
527
585
  Send a request to the OpenAI Responses API.
@@ -538,7 +596,7 @@ class OpenAIResponses(Model):
538
596
 
539
597
  provider_response = self.get_client().responses.create(
540
598
  model=self.id,
541
- input=self._format_messages(messages), # type: ignore
599
+ input=self._format_messages(messages, compress_tool_results), # type: ignore
542
600
  **request_params,
543
601
  )
544
602
 
@@ -579,6 +637,9 @@ class OpenAIResponses(Model):
579
637
  model_name=self.name,
580
638
  model_id=self.id,
581
639
  ) from exc
640
+ except ModelAuthenticationError as exc:
641
+ log_error(f"Model authentication error from OpenAI API: {exc}")
642
+ raise exc
582
643
  except Exception as exc:
583
644
  log_error(f"Error from OpenAI API: {exc}")
584
645
  raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
@@ -591,6 +652,7 @@ class OpenAIResponses(Model):
591
652
  tools: Optional[List[Dict[str, Any]]] = None,
592
653
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
593
654
  run_response: Optional[RunOutput] = None,
655
+ compress_tool_results: bool = False,
594
656
  ) -> ModelResponse:
595
657
  """
596
658
  Sends an asynchronous request to the OpenAI Responses API.
@@ -607,7 +669,7 @@ class OpenAIResponses(Model):
607
669
 
608
670
  provider_response = await self.get_async_client().responses.create(
609
671
  model=self.id,
610
- input=self._format_messages(messages), # type: ignore
672
+ input=self._format_messages(messages, compress_tool_results), # type: ignore
611
673
  **request_params,
612
674
  )
613
675
 
@@ -648,6 +710,9 @@ class OpenAIResponses(Model):
648
710
  model_name=self.name,
649
711
  model_id=self.id,
650
712
  ) from exc
713
+ except ModelAuthenticationError as exc:
714
+ log_error(f"Model authentication error from OpenAI API: {exc}")
715
+ raise exc
651
716
  except Exception as exc:
652
717
  log_error(f"Error from OpenAI API: {exc}")
653
718
  raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
@@ -660,6 +725,7 @@ class OpenAIResponses(Model):
660
725
  tools: Optional[List[Dict[str, Any]]] = None,
661
726
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
662
727
  run_response: Optional[RunOutput] = None,
728
+ compress_tool_results: bool = False,
663
729
  ) -> Iterator[ModelResponse]:
664
730
  """
665
731
  Send a streaming request to the OpenAI Responses API.
@@ -677,7 +743,7 @@ class OpenAIResponses(Model):
677
743
 
678
744
  for chunk in self.get_client().responses.create(
679
745
  model=self.id,
680
- input=self._format_messages(messages), # type: ignore
746
+ input=self._format_messages(messages, compress_tool_results), # type: ignore
681
747
  stream=True,
682
748
  **request_params,
683
749
  ):
@@ -721,6 +787,9 @@ class OpenAIResponses(Model):
721
787
  model_name=self.name,
722
788
  model_id=self.id,
723
789
  ) from exc
790
+ except ModelAuthenticationError as exc:
791
+ log_error(f"Model authentication error from OpenAI API: {exc}")
792
+ raise exc
724
793
  except Exception as exc:
725
794
  log_error(f"Error from OpenAI API: {exc}")
726
795
  raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
@@ -733,6 +802,7 @@ class OpenAIResponses(Model):
733
802
  tools: Optional[List[Dict[str, Any]]] = None,
734
803
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
735
804
  run_response: Optional[RunOutput] = None,
805
+ compress_tool_results: bool = False,
736
806
  ) -> AsyncIterator[ModelResponse]:
737
807
  """
738
808
  Sends an asynchronous streaming request to the OpenAI Responses API.
@@ -750,7 +820,7 @@ class OpenAIResponses(Model):
750
820
 
751
821
  async_stream = await self.get_async_client().responses.create(
752
822
  model=self.id,
753
- input=self._format_messages(messages), # type: ignore
823
+ input=self._format_messages(messages, compress_tool_results), # type: ignore
754
824
  stream=True,
755
825
  **request_params,
756
826
  )
@@ -791,12 +861,19 @@ class OpenAIResponses(Model):
791
861
  model_name=self.name,
792
862
  model_id=self.id,
793
863
  ) from exc
864
+ except ModelAuthenticationError as exc:
865
+ log_error(f"Model authentication error from OpenAI API: {exc}")
866
+ raise exc
794
867
  except Exception as exc:
795
868
  log_error(f"Error from OpenAI API: {exc}")
796
869
  raise ModelProviderError(message=str(exc), model_name=self.name, model_id=self.id) from exc
797
870
 
798
871
  def format_function_call_results(
799
- self, messages: List[Message], function_call_results: List[Message], tool_call_ids: List[str]
872
+ self,
873
+ messages: List[Message],
874
+ function_call_results: List[Message],
875
+ tool_call_ids: List[str],
876
+ compress_tool_results: bool = False,
800
877
  ) -> None:
801
878
  """
802
879
  Handle the results of function calls.
@@ -805,6 +882,7 @@ class OpenAIResponses(Model):
805
882
  messages (List[Message]): The list of conversation messages.
806
883
  function_call_results (List[Message]): The results of the function calls.
807
884
  tool_ids (List[str]): The tool ids.
885
+ compress_tool_results (bool): Whether to compress tool results.
808
886
  """
809
887
  if len(function_call_results) > 0:
810
888
  for _fc_message_index, _fc_message in enumerate(function_call_results):
@@ -1,5 +1,7 @@
1
1
  from agno.models.openrouter.openrouter import OpenRouter
2
+ from agno.models.openrouter.responses import OpenRouterResponses
2
3
 
3
4
  __all__ = [
4
5
  "OpenRouter",
6
+ "OpenRouterResponses",
5
7
  ]
@@ -1,10 +1,14 @@
1
- from dataclasses import dataclass, field
1
+ from dataclasses import dataclass
2
2
  from os import getenv
3
3
  from typing import Any, Dict, List, Optional, Type, Union
4
4
 
5
+ from openai.types.chat import ChatCompletion, ChatCompletionChunk
5
6
  from pydantic import BaseModel
6
7
 
8
+ from agno.exceptions import ModelAuthenticationError
9
+ from agno.models.message import Message
7
10
  from agno.models.openai.like import OpenAILike
11
+ from agno.models.response import ModelResponse
8
12
  from agno.run.agent import RunOutput
9
13
 
10
14
 
@@ -29,11 +33,29 @@ class OpenRouter(OpenAILike):
29
33
  name: str = "OpenRouter"
30
34
  provider: str = "OpenRouter"
31
35
 
32
- api_key: Optional[str] = field(default_factory=lambda: getenv("OPENROUTER_API_KEY"))
36
+ api_key: Optional[str] = None
33
37
  base_url: str = "https://openrouter.ai/api/v1"
34
38
  max_tokens: int = 1024
35
39
  models: Optional[List[str]] = None # Dynamic model routing https://openrouter.ai/docs/features/model-routing
36
40
 
41
+ def _get_client_params(self) -> Dict[str, Any]:
42
+ """
43
+ Returns client parameters for API requests, checking for OPENROUTER_API_KEY.
44
+
45
+ Returns:
46
+ Dict[str, Any]: A dictionary of client parameters for API requests.
47
+ """
48
+ # Fetch API key from env if not already set
49
+ if not self.api_key:
50
+ self.api_key = getenv("OPENROUTER_API_KEY")
51
+ if not self.api_key:
52
+ raise ModelAuthenticationError(
53
+ message="OPENROUTER_API_KEY not set. Please set the OPENROUTER_API_KEY environment variable.",
54
+ model_name=self.name,
55
+ )
56
+
57
+ return super()._get_client_params()
58
+
37
59
  def get_request_params(
38
60
  self,
39
61
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
@@ -64,3 +86,46 @@ class OpenRouter(OpenAILike):
64
86
  request_params["extra_body"] = extra_body
65
87
 
66
88
  return request_params
89
+
90
+ def _format_message(self, message: Message, compress_tool_results: bool = False) -> Dict[str, Any]:
91
+ message_dict = super()._format_message(message, compress_tool_results)
92
+
93
+ if message.role == "assistant" and message.provider_data:
94
+ if message.provider_data.get("reasoning_details"):
95
+ message_dict["reasoning_details"] = message.provider_data["reasoning_details"]
96
+
97
+ return message_dict
98
+
99
+ def _parse_provider_response(
100
+ self,
101
+ response: ChatCompletion,
102
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
103
+ ) -> ModelResponse:
104
+ model_response = super()._parse_provider_response(response, response_format)
105
+
106
+ if response.choices and len(response.choices) > 0:
107
+ response_message = response.choices[0].message
108
+ if hasattr(response_message, "reasoning_details") and response_message.reasoning_details:
109
+ if model_response.provider_data is None:
110
+ model_response.provider_data = {}
111
+ model_response.provider_data["reasoning_details"] = response_message.reasoning_details
112
+ elif hasattr(response_message, "model_extra"):
113
+ extra = getattr(response_message, "model_extra", None)
114
+ if extra and isinstance(extra, dict) and extra.get("reasoning_details"):
115
+ if model_response.provider_data is None:
116
+ model_response.provider_data = {}
117
+ model_response.provider_data["reasoning_details"] = extra["reasoning_details"]
118
+
119
+ return model_response
120
+
121
+ def _parse_provider_response_delta(self, response_delta: ChatCompletionChunk) -> ModelResponse:
122
+ model_response = super()._parse_provider_response_delta(response_delta)
123
+
124
+ if response_delta.choices and len(response_delta.choices) > 0:
125
+ choice_delta = response_delta.choices[0].delta
126
+ if hasattr(choice_delta, "reasoning_details") and choice_delta.reasoning_details:
127
+ if model_response.provider_data is None:
128
+ model_response.provider_data = {}
129
+ model_response.provider_data["reasoning_details"] = choice_delta.reasoning_details
130
+
131
+ return model_response
@@ -0,0 +1,146 @@
1
+ from dataclasses import dataclass
2
+ from os import getenv
3
+ from typing import Any, Dict, List, Optional, Type, Union
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from agno.exceptions import ModelAuthenticationError
8
+ from agno.models.message import Message
9
+ from agno.models.openai.open_responses import OpenResponses
10
+
11
+
12
+ @dataclass
13
+ class OpenRouterResponses(OpenResponses):
14
+ """
15
+ A class for interacting with OpenRouter models using the OpenAI Responses API.
16
+
17
+ OpenRouter's Responses API (currently in beta) provides OpenAI-compatible access
18
+ to multiple AI models through a unified interface. It supports tools, reasoning,
19
+ streaming, and plugins.
20
+
21
+ Note: OpenRouter's Responses API is stateless - each request is independent and
22
+ no server-side state is persisted.
23
+
24
+ For more information, see: https://openrouter.ai/docs/api/reference/responses/overview
25
+
26
+ Attributes:
27
+ id (str): The model id. Defaults to "openai/gpt-oss-20b".
28
+ name (str): The model name. Defaults to "OpenRouterResponses".
29
+ provider (str): The provider name. Defaults to "OpenRouter".
30
+ api_key (Optional[str]): The API key. Uses OPENROUTER_API_KEY env var if not set.
31
+ base_url (str): The base URL. Defaults to "https://openrouter.ai/api/v1".
32
+ models (Optional[List[str]]): List of fallback model IDs to use if the primary model
33
+ fails due to rate limits, timeouts, or unavailability. OpenRouter will automatically
34
+ try these models in order. Example: ["anthropic/claude-sonnet-4", "deepseek/deepseek-r1"]
35
+
36
+ Example:
37
+ ```python
38
+ from agno.agent import Agent
39
+ from agno.models.openrouter import OpenRouterResponses
40
+
41
+ agent = Agent(
42
+ model=OpenRouterResponses(id="anthropic/claude-sonnet-4"),
43
+ markdown=True,
44
+ )
45
+ agent.print_response("Write a haiku about coding")
46
+ ```
47
+ """
48
+
49
+ id: str = "openai/gpt-oss-20b"
50
+ name: str = "OpenRouterResponses"
51
+ provider: str = "OpenRouter"
52
+
53
+ api_key: Optional[str] = None
54
+ base_url: str = "https://openrouter.ai/api/v1"
55
+
56
+ # Dynamic model routing - fallback models if primary fails
57
+ # https://openrouter.ai/docs/features/model-routing
58
+ models: Optional[List[str]] = None
59
+
60
+ # OpenRouter's Responses API is stateless
61
+ store: Optional[bool] = False
62
+
63
+ def _get_client_params(self) -> Dict[str, Any]:
64
+ """
65
+ Returns client parameters for API requests, checking for OPENROUTER_API_KEY.
66
+
67
+ Returns:
68
+ Dict[str, Any]: A dictionary of client parameters for API requests.
69
+
70
+ Raises:
71
+ ModelAuthenticationError: If OPENROUTER_API_KEY is not set.
72
+ """
73
+ # Fetch API key from env if not already set
74
+ if not self.api_key:
75
+ self.api_key = getenv("OPENROUTER_API_KEY")
76
+ if not self.api_key:
77
+ raise ModelAuthenticationError(
78
+ message="OPENROUTER_API_KEY not set. Please set the OPENROUTER_API_KEY environment variable.",
79
+ model_name=self.name,
80
+ )
81
+
82
+ # Build client params
83
+ base_params: Dict[str, Any] = {
84
+ "api_key": self.api_key,
85
+ "base_url": self.base_url,
86
+ "organization": self.organization,
87
+ "timeout": self.timeout,
88
+ "max_retries": self.max_retries,
89
+ "default_headers": self.default_headers,
90
+ "default_query": self.default_query,
91
+ }
92
+
93
+ # Filter out None values
94
+ client_params = {k: v for k, v in base_params.items() if v is not None}
95
+
96
+ # Add additional client params if provided
97
+ if self.client_params:
98
+ client_params.update(self.client_params)
99
+
100
+ return client_params
101
+
102
+ def get_request_params(
103
+ self,
104
+ messages: Optional[List[Message]] = None,
105
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
106
+ tools: Optional[List[Dict[str, Any]]] = None,
107
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
108
+ ) -> Dict[str, Any]:
109
+ """
110
+ Returns keyword arguments for API requests, including fallback models configuration.
111
+
112
+ Returns:
113
+ Dict[str, Any]: A dictionary of keyword arguments for API requests.
114
+ """
115
+ # Get base request params from parent class
116
+ request_params = super().get_request_params(
117
+ messages=messages,
118
+ response_format=response_format,
119
+ tools=tools,
120
+ tool_choice=tool_choice,
121
+ )
122
+
123
+ # Add fallback models to extra_body if specified
124
+ if self.models:
125
+ # Get existing extra_body or create new dict
126
+ extra_body = request_params.get("extra_body") or {}
127
+
128
+ # Merge fallback models into extra_body
129
+ extra_body["models"] = self.models
130
+
131
+ # Update request params
132
+ request_params["extra_body"] = extra_body
133
+
134
+ return request_params
135
+
136
+ def _using_reasoning_model(self) -> bool:
137
+ """
138
+ Check if the model is a reasoning model that requires special handling.
139
+
140
+ OpenRouter hosts various reasoning models, but they may not all use
141
+ OpenAI's reasoning API format. We check for known reasoning model patterns.
142
+ """
143
+ # Check for OpenAI reasoning models hosted on OpenRouter
144
+ if self.id.startswith("openai/o3") or self.id.startswith("openai/o4"):
145
+ return True
146
+ return False
@@ -4,7 +4,7 @@ from typing import Any, Dict, Optional, Type, Union
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
- from agno.exceptions import ModelProviderError
7
+ from agno.exceptions import ModelAuthenticationError, ModelProviderError
8
8
  from agno.models.message import Citations, UrlCitation
9
9
  from agno.models.metrics import Metrics
10
10
  from agno.models.response import ModelResponse
@@ -41,6 +41,8 @@ class Perplexity(OpenAILike):
41
41
  id: str = "sonar"
42
42
  name: str = "Perplexity"
43
43
  provider: str = "Perplexity"
44
+ # Perplexity returns cumulative token counts in each streaming chunk, so only collect on final chunk
45
+ collect_metrics_on_completion: bool = True
44
46
 
45
47
  api_key: Optional[str] = field(default_factory=lambda: getenv("PERPLEXITY_API_KEY"))
46
48
  base_url: str = "https://api.perplexity.ai/"
@@ -50,6 +52,22 @@ class Perplexity(OpenAILike):
50
52
  supports_native_structured_outputs: bool = False
51
53
  supports_json_schema_outputs: bool = True
52
54
 
55
+ def _get_client_params(self) -> Dict[str, Any]:
56
+ """
57
+ Returns client parameters for API requests, checking for PERPLEXITY_API_KEY.
58
+
59
+ Returns:
60
+ Dict[str, Any]: A dictionary of client parameters for API requests.
61
+ """
62
+ if not self.api_key:
63
+ self.api_key = getenv("PERPLEXITY_API_KEY")
64
+ if not self.api_key:
65
+ raise ModelAuthenticationError(
66
+ message="PERPLEXITY_API_KEY not set. Please set the PERPLEXITY_API_KEY environment variable.",
67
+ model_name=self.name,
68
+ )
69
+ return super()._get_client_params()
70
+
53
71
  def get_request_params(
54
72
  self,
55
73
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,