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.
- agno/agent/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
agno/models/openai/responses.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
#
|
|
237
|
-
text_params["format"] =
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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":
|
|
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,
|
|
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,10 +1,14 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
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] =
|
|
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,
|