solace-agent-mesh 1.5.1__py3-none-any.whl → 1.6.1__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.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/adk/callbacks.py +0 -5
- solace_agent_mesh/agent/adk/models/lite_llm.py +123 -8
- solace_agent_mesh/agent/adk/models/oauth2_token_manager.py +245 -0
- solace_agent_mesh/agent/protocol/event_handlers.py +213 -31
- solace_agent_mesh/agent/proxies/__init__.py +0 -0
- solace_agent_mesh/agent/proxies/a2a/__init__.py +3 -0
- solace_agent_mesh/agent/proxies/a2a/app.py +55 -0
- solace_agent_mesh/agent/proxies/a2a/component.py +1115 -0
- solace_agent_mesh/agent/proxies/a2a/config.py +140 -0
- solace_agent_mesh/agent/proxies/a2a/oauth_token_cache.py +104 -0
- solace_agent_mesh/agent/proxies/base/__init__.py +3 -0
- solace_agent_mesh/agent/proxies/base/app.py +99 -0
- solace_agent_mesh/agent/proxies/base/component.py +650 -0
- solace_agent_mesh/agent/proxies/base/config.py +85 -0
- solace_agent_mesh/agent/proxies/base/proxy_task_context.py +17 -0
- solace_agent_mesh/agent/sac/app.py +58 -5
- solace_agent_mesh/agent/sac/component.py +238 -75
- solace_agent_mesh/agent/sac/task_execution_context.py +46 -0
- solace_agent_mesh/agent/tools/audio_tools.py +125 -8
- solace_agent_mesh/agent/tools/web_tools.py +10 -5
- solace_agent_mesh/agent/utils/artifact_helpers.py +141 -3
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.eda4bcb2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.f4b15f3b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/71da7b71.38583438.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/77cf947d.48cb18a2.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/924ffdeb.8095e148.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/9e9d0a82.570c057b.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{ad71b5ed.60668e9e.js → ad71b5ed.af3ecfd1.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/ceb2a7a6.5d92d7d0.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{da0b5bad.9d369087.js → da0b5bad.d08a9466.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/db924877.e98d12a1.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/de915948.27d6b065.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{e3d9abda.2b916f9e.js → e3d9abda.6b9493d0.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.e74a984d.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/f284c35a.42f59cdd.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.15b02f97.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js → main.b12eac43.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/runtime~main.e268214e.js +1 -0
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +15 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +262 -0
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +31 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +135 -0
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +6 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +5 -5
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +4 -4
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +6 -5
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +100 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1761248203150.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/assets/docs/sitemap.xml +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/add_cmd/agent_cmd.py +2 -69
- solace_agent_mesh/cli/commands/eval_cmd.py +11 -49
- solace_agent_mesh/cli/commands/init_cmd/__init__.py +0 -5
- solace_agent_mesh/cli/commands/init_cmd/env_step.py +10 -12
- solace_agent_mesh/cli/commands/init_cmd/orchestrator_step.py +9 -61
- solace_agent_mesh/cli/commands/init_cmd/webui_gateway_step.py +9 -49
- solace_agent_mesh/cli/commands/plugin_cmd/add_cmd.py +1 -2
- solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-DwrxZE0E.js → authCallback-BTf6dqwp.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/{client-DarGQzyw.js → client-CaY59VuC.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +342 -0
- solace_agent_mesh/client/webui/frontend/static/assets/main-DHJKSW1S.css +1 -0
- solace_agent_mesh/client/webui/frontend/static/assets/{vendor-BKIeiHj_.js → vendor-BEmvJSYz.js} +1 -1
- solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
- solace_agent_mesh/client/webui/frontend/static/index.html +4 -4
- solace_agent_mesh/common/a2a/__init__.py +24 -0
- solace_agent_mesh/common/a2a/artifact.py +39 -0
- solace_agent_mesh/common/a2a/events.py +29 -0
- solace_agent_mesh/common/a2a/message.py +68 -0
- solace_agent_mesh/common/a2a/protocol.py +151 -1
- solace_agent_mesh/common/agent_registry.py +83 -3
- solace_agent_mesh/common/constants.py +3 -1
- solace_agent_mesh/common/sac/sam_component_base.py +383 -4
- solace_agent_mesh/common/utils/pydantic_utils.py +12 -0
- solace_agent_mesh/config_portal/backend/common.py +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-ByU1X1HD.js +98 -0
- solace_agent_mesh/config_portal/frontend/static/client/assets/{manifest-44d62be6.js → manifest-61038fc6.js} +1 -1
- solace_agent_mesh/config_portal/frontend/static/client/index.html +1 -1
- solace_agent_mesh/evaluation/evaluator.py +128 -104
- solace_agent_mesh/evaluation/message_organizer.py +116 -110
- solace_agent_mesh/evaluation/report_data_processor.py +84 -86
- solace_agent_mesh/evaluation/report_generator.py +73 -79
- solace_agent_mesh/evaluation/run.py +421 -235
- solace_agent_mesh/evaluation/shared/__init__.py +92 -0
- solace_agent_mesh/evaluation/shared/constants.py +47 -0
- solace_agent_mesh/evaluation/shared/exceptions.py +50 -0
- solace_agent_mesh/evaluation/shared/helpers.py +35 -0
- solace_agent_mesh/evaluation/shared/test_case_loader.py +167 -0
- solace_agent_mesh/evaluation/shared/test_suite_loader.py +280 -0
- solace_agent_mesh/evaluation/subscriber.py +111 -232
- solace_agent_mesh/evaluation/summary_builder.py +227 -117
- solace_agent_mesh/gateway/base/app.py +16 -1
- solace_agent_mesh/gateway/base/component.py +112 -39
- solace_agent_mesh/gateway/http_sse/alembic/versions/20251015_add_session_performance_indexes.py +70 -0
- solace_agent_mesh/gateway/http_sse/component.py +99 -3
- solace_agent_mesh/gateway/http_sse/dependencies.py +4 -4
- solace_agent_mesh/gateway/http_sse/main.py +1 -0
- solace_agent_mesh/gateway/http_sse/repository/chat_task_repository.py +12 -13
- solace_agent_mesh/gateway/http_sse/repository/feedback_repository.py +15 -18
- solace_agent_mesh/gateway/http_sse/repository/interfaces.py +25 -18
- solace_agent_mesh/gateway/http_sse/repository/session_repository.py +30 -26
- solace_agent_mesh/gateway/http_sse/repository/task_repository.py +35 -44
- solace_agent_mesh/gateway/http_sse/routers/agent_cards.py +4 -3
- solace_agent_mesh/gateway/http_sse/routers/artifacts.py +95 -203
- solace_agent_mesh/gateway/http_sse/routers/dto/responses/session_responses.py +4 -3
- solace_agent_mesh/gateway/http_sse/routers/sessions.py +2 -2
- solace_agent_mesh/gateway/http_sse/routers/tasks.py +33 -41
- solace_agent_mesh/gateway/http_sse/routers/users.py +47 -1
- solace_agent_mesh/gateway/http_sse/routers/visualization.py +17 -11
- solace_agent_mesh/gateway/http_sse/services/data_retention_service.py +4 -4
- solace_agent_mesh/gateway/http_sse/services/feedback_service.py +51 -43
- solace_agent_mesh/gateway/http_sse/services/session_service.py +20 -20
- solace_agent_mesh/gateway/http_sse/services/task_logger_service.py +8 -8
- solace_agent_mesh/gateway/http_sse/shared/base_repository.py +45 -71
- solace_agent_mesh/gateway/http_sse/shared/types.py +0 -18
- solace_agent_mesh/templates/gateway_config_template.yaml +0 -5
- solace_agent_mesh/templates/logging_config_template.ini +10 -6
- solace_agent_mesh/templates/plugin_gateway_config_template.yaml +0 -3
- solace_agent_mesh/templates/shared_config.yaml +40 -0
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/METADATA +47 -21
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/RECORD +166 -145
- solace_agent_mesh/assets/docs/assets/js/5c2bd65f.e49689dd.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/6ad8f0bd.39d5851d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/71da7b71.804d6567.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/77cf947d.64c9bd6c.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/9e9d0a82.dd810042.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/db924877.cbc66f02.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/de915948.139b4b9c.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/e6f9706b.582a78ca.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/f284c35a.5766a13d.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/ff4d71f2.9c0297a6.js +0 -1
- solace_agent_mesh/assets/docs/assets/js/runtime~main.18dc45dd.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1760121512891.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1760121512891.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-2nd1gbaH.js +0 -339
- solace_agent_mesh/client/webui/frontend/static/assets/main-DoKXctCM.css +0 -1
- solace_agent_mesh/config_portal/frontend/static/client/assets/_index-BNuqpWDc.js +0 -98
- solace_agent_mesh/evaluation/config_loader.py +0 -657
- solace_agent_mesh/evaluation/test_case_loader.py +0 -714
- /solace_agent_mesh/assets/docs/assets/js/{main.bd3c34f3.js.LICENSE.txt → main.b12eac43.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.5.1.dist-info → solace_agent_mesh-1.6.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1094,11 +1094,6 @@ If a plan is created:
|
|
|
1094
1094
|
e_last_call,
|
|
1095
1095
|
)
|
|
1096
1096
|
|
|
1097
|
-
if host_component.get_config("inject_current_time", True):
|
|
1098
|
-
current_time = datetime.now(timezone.utc).strftime("%A, %d %b %Y %H:%M:%S UTC")
|
|
1099
|
-
instruction = f"Current time {current_time}."
|
|
1100
|
-
injected_instructions.append(instruction)
|
|
1101
|
-
|
|
1102
1097
|
if injected_instructions:
|
|
1103
1098
|
combined_instructions = "\n\n---\n\n".join(injected_instructions)
|
|
1104
1099
|
if llm_request.config is None:
|
|
@@ -53,6 +53,7 @@ from typing_extensions import override
|
|
|
53
53
|
from google.adk.models.base_llm import BaseLlm
|
|
54
54
|
from google.adk.models.llm_request import LlmRequest
|
|
55
55
|
from google.adk.models.llm_response import LlmResponse
|
|
56
|
+
from .oauth2_token_manager import OAuth2ClientCredentialsTokenManager
|
|
56
57
|
|
|
57
58
|
logger = logging.getLogger("google_adk." + __name__)
|
|
58
59
|
|
|
@@ -479,6 +480,7 @@ def _message_to_generate_content_response(
|
|
|
479
480
|
|
|
480
481
|
def _get_completion_inputs(
|
|
481
482
|
llm_request: LlmRequest,
|
|
483
|
+
cache_strategy: str = "5m",
|
|
482
484
|
) -> Tuple[
|
|
483
485
|
List[Message],
|
|
484
486
|
Optional[List[Dict]],
|
|
@@ -489,6 +491,7 @@ def _get_completion_inputs(
|
|
|
489
491
|
|
|
490
492
|
Args:
|
|
491
493
|
llm_request: The LlmRequest to convert.
|
|
494
|
+
cache_strategy: Cache strategy to apply ("none", "5m", "1h").
|
|
492
495
|
|
|
493
496
|
Returns:
|
|
494
497
|
The litellm inputs (message list, tool dictionary and response format).
|
|
@@ -501,16 +504,32 @@ def _get_completion_inputs(
|
|
|
501
504
|
elif message_param_or_list: # Ensure it's not None before appending
|
|
502
505
|
messages.append(message_param_or_list)
|
|
503
506
|
|
|
504
|
-
if llm_request.config.system_instruction:
|
|
507
|
+
if llm_request.config and llm_request.config.system_instruction:
|
|
508
|
+
# Build system instruction content with optional cache control
|
|
509
|
+
system_content = {
|
|
510
|
+
"type": "text",
|
|
511
|
+
"text": llm_request.config.system_instruction,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
# Add cache control based on strategy
|
|
515
|
+
# LiteLLM translates this to provider-specific format (Anthropic, OpenAI, Bedrock, Deepseek)
|
|
516
|
+
if cache_strategy == "5m":
|
|
517
|
+
# 5-minute ephemeral cache (Anthropic default)
|
|
518
|
+
system_content["cache_control"] = {"type": "ephemeral"}
|
|
519
|
+
elif cache_strategy == "1h":
|
|
520
|
+
# 1-hour extended cache (Anthropic extended)
|
|
521
|
+
system_content["cache_control"] = {"type": "ephemeral", "ttl": "1h"}
|
|
522
|
+
# For "none", no cache_control is added
|
|
523
|
+
|
|
505
524
|
messages.insert(
|
|
506
525
|
0,
|
|
507
526
|
ChatCompletionDeveloperMessage(
|
|
508
527
|
role="developer",
|
|
509
|
-
content=
|
|
528
|
+
content=[system_content],
|
|
510
529
|
),
|
|
511
530
|
)
|
|
512
531
|
|
|
513
|
-
# 2. Convert tool declarations
|
|
532
|
+
# 2. Convert tool declarations with caching support
|
|
514
533
|
tools: Optional[List[Dict]] = None
|
|
515
534
|
if (
|
|
516
535
|
llm_request.config
|
|
@@ -522,6 +541,16 @@ def _get_completion_inputs(
|
|
|
522
541
|
for tool in llm_request.config.tools[0].function_declarations
|
|
523
542
|
]
|
|
524
543
|
|
|
544
|
+
# Enable tool caching via LiteLLM's generic interface
|
|
545
|
+
# LiteLLM handles provider-specific translation (Anthropic, OpenAI, Bedrock, Deepseek)
|
|
546
|
+
# Tools are stable because peer agents are alphabetically sorted (component.py)
|
|
547
|
+
if tools and cache_strategy != "none":
|
|
548
|
+
# Add cache_control to the LAST tool (required by caching providers)
|
|
549
|
+
if cache_strategy == "5m":
|
|
550
|
+
tools[-1]["cache_control"] = {"type": "ephemeral"}
|
|
551
|
+
elif cache_strategy == "1h":
|
|
552
|
+
tools[-1]["cache_control"] = {"type": "ephemeral", "ttl": "1h"}
|
|
553
|
+
|
|
525
554
|
# 3. Handle response format
|
|
526
555
|
response_format: Optional[types.SchemaUnion] = None
|
|
527
556
|
if llm_request.config and llm_request.config.response_schema:
|
|
@@ -595,7 +624,7 @@ def _build_request_log(req: LlmRequest) -> str:
|
|
|
595
624
|
|
|
596
625
|
function_decls: list[types.FunctionDeclaration] = cast(
|
|
597
626
|
list[types.FunctionDeclaration],
|
|
598
|
-
req.config.tools[0].function_declarations if req.config.tools else [],
|
|
627
|
+
req.config.tools[0].function_declarations if req.config and req.config.tools else [],
|
|
599
628
|
)
|
|
600
629
|
function_logs = (
|
|
601
630
|
[_build_function_declaration_log(func_decl) for func_decl in function_decls]
|
|
@@ -616,7 +645,7 @@ def _build_request_log(req: LlmRequest) -> str:
|
|
|
616
645
|
LLM Request:
|
|
617
646
|
-----------------------------------------------------------
|
|
618
647
|
System Instruction:
|
|
619
|
-
{req.config.system_instruction}
|
|
648
|
+
{req.config.system_instruction if req.config else None}
|
|
620
649
|
-----------------------------------------------------------
|
|
621
650
|
Contents:
|
|
622
651
|
{_NEW_LINE.join(contents_logs)}
|
|
@@ -654,16 +683,42 @@ class LiteLlm(BaseLlm):
|
|
|
654
683
|
"""The LLM client to use for the model."""
|
|
655
684
|
|
|
656
685
|
_additional_args: Dict[str, Any] = None
|
|
686
|
+
_oauth_token_manager: Optional[OAuth2ClientCredentialsTokenManager] = None
|
|
687
|
+
_cache_strategy: str = "5m" # Default to 5-minute ephemeral cache
|
|
657
688
|
|
|
658
|
-
def __init__(self, model: str, **kwargs):
|
|
689
|
+
def __init__(self, model: str, cache_strategy: str = "5m", **kwargs):
|
|
659
690
|
"""Initializes the LiteLlm class.
|
|
660
691
|
|
|
661
692
|
Args:
|
|
662
693
|
model: The name of the LiteLlm model.
|
|
694
|
+
cache_strategy: Cache strategy to use. Options: "none", "5m" (ephemeral), "1h" (extended).
|
|
695
|
+
Defaults to "5m" for backward compatibility.
|
|
663
696
|
**kwargs: Additional arguments to pass to the litellm completion api.
|
|
697
|
+
Can include OAuth configuration parameters.
|
|
664
698
|
"""
|
|
665
699
|
super().__init__(model=model, **kwargs)
|
|
666
|
-
self._additional_args = kwargs
|
|
700
|
+
self._additional_args = kwargs.copy()
|
|
701
|
+
|
|
702
|
+
# Validate and store cache strategy
|
|
703
|
+
valid_strategies = ["none", "5m", "1h"]
|
|
704
|
+
if cache_strategy not in valid_strategies:
|
|
705
|
+
logger.warning(
|
|
706
|
+
"Invalid cache_strategy '%s'. Valid options are: %s. Defaulting to '5m'.",
|
|
707
|
+
cache_strategy,
|
|
708
|
+
valid_strategies,
|
|
709
|
+
)
|
|
710
|
+
cache_strategy = "5m"
|
|
711
|
+
self._cache_strategy = cache_strategy
|
|
712
|
+
logger.info("LiteLlm initialized with cache strategy: %s", self._cache_strategy)
|
|
713
|
+
|
|
714
|
+
# Extract OAuth configuration if present
|
|
715
|
+
oauth_config = self._extract_oauth_config(self._additional_args)
|
|
716
|
+
if oauth_config:
|
|
717
|
+
self._oauth_token_manager = OAuth2ClientCredentialsTokenManager(**oauth_config)
|
|
718
|
+
logger.info("OAuth2 token manager initialized for model: %s", model)
|
|
719
|
+
else:
|
|
720
|
+
self._oauth_token_manager = None
|
|
721
|
+
|
|
667
722
|
# preventing generation call with llm_client
|
|
668
723
|
# and overriding messages, tools and stream which are managed internally
|
|
669
724
|
self._additional_args.pop("llm_client", None)
|
|
@@ -672,6 +727,48 @@ class LiteLlm(BaseLlm):
|
|
|
672
727
|
# public api called from runner determines to stream or not
|
|
673
728
|
self._additional_args.pop("stream", None)
|
|
674
729
|
|
|
730
|
+
def _extract_oauth_config(self, kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
731
|
+
"""Extract OAuth configuration from kwargs.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
kwargs: Keyword arguments that may contain OAuth parameters
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
OAuth configuration dictionary or None if no OAuth config found
|
|
738
|
+
"""
|
|
739
|
+
oauth_params = [
|
|
740
|
+
"oauth_token_url",
|
|
741
|
+
"oauth_client_id",
|
|
742
|
+
"oauth_client_secret",
|
|
743
|
+
"oauth_scope",
|
|
744
|
+
"oauth_ca_cert",
|
|
745
|
+
"oauth_token_refresh_buffer_seconds",
|
|
746
|
+
"oauth_max_retries"
|
|
747
|
+
]
|
|
748
|
+
|
|
749
|
+
oauth_config = {}
|
|
750
|
+
for param in oauth_params:
|
|
751
|
+
if param in kwargs:
|
|
752
|
+
# Map parameter names to OAuth2ClientCredentialsTokenManager constructor
|
|
753
|
+
if param == "oauth_ca_cert":
|
|
754
|
+
oauth_config["ca_cert_path"] = kwargs.pop(param)
|
|
755
|
+
elif param == "oauth_token_refresh_buffer_seconds":
|
|
756
|
+
oauth_config["refresh_buffer_seconds"] = kwargs.pop(param)
|
|
757
|
+
elif param == "oauth_max_retries":
|
|
758
|
+
oauth_config["max_retries"] = kwargs.pop(param)
|
|
759
|
+
else:
|
|
760
|
+
# Remove oauth_ prefix for the token manager
|
|
761
|
+
key = param.replace("oauth_", "")
|
|
762
|
+
oauth_config[key] = kwargs.pop(param)
|
|
763
|
+
|
|
764
|
+
# Return config only if we have the required parameters
|
|
765
|
+
if "token_url" in oauth_config and "client_id" in oauth_config and "client_secret" in oauth_config:
|
|
766
|
+
return oauth_config
|
|
767
|
+
elif oauth_config:
|
|
768
|
+
logger.warning("Incomplete OAuth configuration found, missing required parameters")
|
|
769
|
+
|
|
770
|
+
return None
|
|
771
|
+
|
|
675
772
|
async def generate_content_async(
|
|
676
773
|
self, llm_request: LlmRequest, stream: bool = False
|
|
677
774
|
) -> AsyncGenerator[LlmResponse, None]:
|
|
@@ -693,7 +790,7 @@ class LiteLlm(BaseLlm):
|
|
|
693
790
|
logger.debug(_build_request_log(llm_request))
|
|
694
791
|
|
|
695
792
|
messages, tools, response_format, generation_params = _get_completion_inputs(
|
|
696
|
-
llm_request
|
|
793
|
+
llm_request, self._cache_strategy
|
|
697
794
|
)
|
|
698
795
|
completion_args = {
|
|
699
796
|
"model": self.model,
|
|
@@ -704,6 +801,24 @@ class LiteLlm(BaseLlm):
|
|
|
704
801
|
}
|
|
705
802
|
completion_args.update(self._additional_args)
|
|
706
803
|
|
|
804
|
+
# Inject OAuth token if OAuth is configured
|
|
805
|
+
if self._oauth_token_manager:
|
|
806
|
+
try:
|
|
807
|
+
access_token = await self._oauth_token_manager.get_token()
|
|
808
|
+
# Inject Bearer token via extra_headers
|
|
809
|
+
extra_headers = completion_args.get("extra_headers", {})
|
|
810
|
+
extra_headers["Authorization"] = f"Bearer {access_token}"
|
|
811
|
+
completion_args["extra_headers"] = extra_headers
|
|
812
|
+
logger.debug("OAuth token injected into request headers")
|
|
813
|
+
except Exception as e:
|
|
814
|
+
logger.error("Failed to get OAuth token: %s", str(e))
|
|
815
|
+
# Check if we have a fallback API key
|
|
816
|
+
if "api_key" in completion_args:
|
|
817
|
+
logger.info("Falling back to API key authentication")
|
|
818
|
+
else:
|
|
819
|
+
logger.error("No fallback authentication available")
|
|
820
|
+
raise
|
|
821
|
+
|
|
707
822
|
if generation_params:
|
|
708
823
|
completion_args.update(generation_params)
|
|
709
824
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""OAuth 2.0 Client Credentials Token Manager.
|
|
2
|
+
|
|
3
|
+
This module provides OAuth 2.0 Client Credentials flow implementation for LLM authentication.
|
|
4
|
+
It handles token acquisition, caching, and automatic refresh with proper error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from solace_agent_mesh.common.utils.in_memory_cache import InMemoryCache
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OAuth2ClientCredentialsTokenManager:
|
|
21
|
+
"""Manages OAuth 2.0 Client Credentials tokens with caching and automatic refresh.
|
|
22
|
+
|
|
23
|
+
This class implements the OAuth 2.0 Client Credentials flow as defined in RFC 6749.
|
|
24
|
+
It provides thread-safe token management with automatic refresh before expiration
|
|
25
|
+
and integrates with the existing InMemoryCache for token storage.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
token_url: OAuth 2.0 token endpoint URL
|
|
29
|
+
client_id: OAuth client identifier
|
|
30
|
+
client_secret: OAuth client secret
|
|
31
|
+
scope: OAuth scope (optional)
|
|
32
|
+
ca_cert_path: Path to custom CA certificate (optional)
|
|
33
|
+
refresh_buffer_seconds: Seconds before expiry to refresh token
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
token_url: str,
|
|
39
|
+
client_id: str,
|
|
40
|
+
client_secret: str,
|
|
41
|
+
scope: Optional[str] = None,
|
|
42
|
+
ca_cert_path: Optional[str] = None,
|
|
43
|
+
refresh_buffer_seconds: int = 300,
|
|
44
|
+
max_retries: int = 3,
|
|
45
|
+
):
|
|
46
|
+
"""Initialize the OAuth2 Client Credentials Token Manager.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
token_url: OAuth 2.0 token endpoint URL
|
|
50
|
+
client_id: OAuth client identifier
|
|
51
|
+
client_secret: OAuth client secret
|
|
52
|
+
scope: OAuth scope (optional, space-separated string)
|
|
53
|
+
ca_cert_path: Path to custom CA certificate file (optional)
|
|
54
|
+
refresh_buffer_seconds: Seconds before actual expiry to refresh token
|
|
55
|
+
max_retries: Maximum number of retry attempts for token requests
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: If required parameters are missing or invalid
|
|
59
|
+
"""
|
|
60
|
+
if not token_url:
|
|
61
|
+
raise ValueError("token_url is required")
|
|
62
|
+
if not client_id:
|
|
63
|
+
raise ValueError("client_id is required")
|
|
64
|
+
if not client_secret:
|
|
65
|
+
raise ValueError("client_secret is required")
|
|
66
|
+
if refresh_buffer_seconds < 0:
|
|
67
|
+
raise ValueError("refresh_buffer_seconds must be non-negative")
|
|
68
|
+
|
|
69
|
+
self.token_url = token_url
|
|
70
|
+
self.client_id = client_id
|
|
71
|
+
self.client_secret = client_secret
|
|
72
|
+
self.scope = scope
|
|
73
|
+
self.ca_cert_path = ca_cert_path
|
|
74
|
+
self.refresh_buffer_seconds = refresh_buffer_seconds
|
|
75
|
+
self.max_retries = max_retries
|
|
76
|
+
|
|
77
|
+
# Thread-safe token access
|
|
78
|
+
self._lock = asyncio.Lock()
|
|
79
|
+
|
|
80
|
+
# Token cache using existing InMemoryCache singleton
|
|
81
|
+
self._cache = InMemoryCache()
|
|
82
|
+
|
|
83
|
+
# Cache key for this token manager instance
|
|
84
|
+
self._cache_key = f"oauth_token_{hash((token_url, client_id))}"
|
|
85
|
+
|
|
86
|
+
logger.info(
|
|
87
|
+
"OAuth2ClientCredentialsTokenManager initialized for endpoint: %s",
|
|
88
|
+
token_url
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
async def get_token(self) -> str:
|
|
92
|
+
"""Get a valid OAuth 2.0 access token.
|
|
93
|
+
|
|
94
|
+
This method checks the cache first and returns a cached token if it's still valid.
|
|
95
|
+
If no token exists or the token is expired/near expiry, it fetches a new token.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Valid OAuth 2.0 access token
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
httpx.HTTPError: If token request fails
|
|
102
|
+
ValueError: If token response is invalid
|
|
103
|
+
"""
|
|
104
|
+
async with self._lock:
|
|
105
|
+
# Check if we have a cached token
|
|
106
|
+
cached_token_data = self._cache.get(self._cache_key)
|
|
107
|
+
|
|
108
|
+
if cached_token_data and not self._is_token_expired(cached_token_data):
|
|
109
|
+
logger.debug("Using cached OAuth token")
|
|
110
|
+
return cached_token_data["access_token"]
|
|
111
|
+
|
|
112
|
+
# Fetch new token
|
|
113
|
+
logger.info("Fetching new OAuth token from %s", self.token_url)
|
|
114
|
+
token_data = await self._fetch_token()
|
|
115
|
+
|
|
116
|
+
# Cache the token with TTL
|
|
117
|
+
expires_in = token_data.get("expires_in", 3600) # Default 1 hour
|
|
118
|
+
cache_ttl = max(expires_in - self.refresh_buffer_seconds, 60) # Min 1 minute
|
|
119
|
+
|
|
120
|
+
self._cache.set(self._cache_key, token_data, ttl=cache_ttl)
|
|
121
|
+
|
|
122
|
+
logger.info("OAuth token cached with TTL: %d seconds", cache_ttl)
|
|
123
|
+
return token_data["access_token"]
|
|
124
|
+
|
|
125
|
+
def _is_token_expired(self, token_data: Dict[str, Any]) -> bool:
|
|
126
|
+
"""Check if a token is expired or near expiry.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
token_data: Token data dictionary with 'expires_at' timestamp
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if token is expired or near expiry, False otherwise
|
|
133
|
+
"""
|
|
134
|
+
if "expires_at" not in token_data:
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
current_time = time.time()
|
|
138
|
+
expires_at = token_data["expires_at"]
|
|
139
|
+
|
|
140
|
+
# Consider token expired if it expires within the buffer time
|
|
141
|
+
return current_time >= (expires_at - self.refresh_buffer_seconds)
|
|
142
|
+
|
|
143
|
+
async def _fetch_token(self) -> Dict[str, Any]:
|
|
144
|
+
"""Fetch a new OAuth 2.0 access token from the token endpoint.
|
|
145
|
+
|
|
146
|
+
Implements retry logic with exponential backoff for transient failures.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Token data dictionary containing access_token, expires_in, etc.
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
httpx.HTTPError: If HTTP request fails after all retries
|
|
153
|
+
ValueError: If response is invalid or missing required fields
|
|
154
|
+
"""
|
|
155
|
+
# Prepare request payload
|
|
156
|
+
payload = {
|
|
157
|
+
"grant_type": "client_credentials",
|
|
158
|
+
"client_id": self.client_id,
|
|
159
|
+
"client_secret": self.client_secret,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if self.scope:
|
|
163
|
+
payload["scope"] = self.scope
|
|
164
|
+
|
|
165
|
+
# Configure HTTP client with SSL settings
|
|
166
|
+
verify = True
|
|
167
|
+
if self.ca_cert_path:
|
|
168
|
+
verify = self.ca_cert_path
|
|
169
|
+
|
|
170
|
+
headers = {
|
|
171
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
172
|
+
"Accept": "application/json",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
last_exception = None
|
|
176
|
+
|
|
177
|
+
for attempt in range(self.max_retries + 1):
|
|
178
|
+
try:
|
|
179
|
+
async with httpx.AsyncClient(verify=verify) as client:
|
|
180
|
+
response = await client.post(
|
|
181
|
+
self.token_url,
|
|
182
|
+
data=payload,
|
|
183
|
+
headers=headers,
|
|
184
|
+
timeout=30.0,
|
|
185
|
+
)
|
|
186
|
+
response.raise_for_status()
|
|
187
|
+
|
|
188
|
+
token_data = response.json()
|
|
189
|
+
|
|
190
|
+
# Validate response
|
|
191
|
+
if "access_token" not in token_data:
|
|
192
|
+
raise ValueError("Token response missing 'access_token' field")
|
|
193
|
+
|
|
194
|
+
# Add expiration timestamp for cache management
|
|
195
|
+
expires_in = token_data.get("expires_in", 3600)
|
|
196
|
+
token_data["expires_at"] = time.time() + expires_in
|
|
197
|
+
|
|
198
|
+
logger.info("Successfully fetched OAuth token, expires in %d seconds", expires_in)
|
|
199
|
+
return token_data
|
|
200
|
+
|
|
201
|
+
except httpx.HTTPStatusError as e:
|
|
202
|
+
last_exception = e
|
|
203
|
+
# Don't retry on 4xx errors (client errors)
|
|
204
|
+
if 400 <= e.response.status_code < 500:
|
|
205
|
+
logger.error(
|
|
206
|
+
"OAuth token request failed with client error %d: %s",
|
|
207
|
+
e.response.status_code,
|
|
208
|
+
e.response.text
|
|
209
|
+
)
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
logger.warning(
|
|
213
|
+
"OAuth token request failed with status %d (attempt %d/%d): %s",
|
|
214
|
+
e.response.status_code,
|
|
215
|
+
attempt + 1,
|
|
216
|
+
self.max_retries + 1,
|
|
217
|
+
e.response.text
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
except httpx.RequestError as e:
|
|
221
|
+
last_exception = e
|
|
222
|
+
logger.warning(
|
|
223
|
+
"OAuth token request failed (attempt %d/%d): %s",
|
|
224
|
+
attempt + 1,
|
|
225
|
+
self.max_retries + 1,
|
|
226
|
+
str(e)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
last_exception = e
|
|
231
|
+
logger.error("Unexpected error during OAuth token fetch: %s", str(e))
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
# Exponential backoff with jitter for retries
|
|
235
|
+
if attempt < self.max_retries:
|
|
236
|
+
delay = (2 ** attempt) + random.uniform(0, 1)
|
|
237
|
+
logger.info("Retrying OAuth token request in %.2f seconds", delay)
|
|
238
|
+
await asyncio.sleep(delay)
|
|
239
|
+
|
|
240
|
+
# All retries exhausted
|
|
241
|
+
logger.error("OAuth token request failed after %d attempts", self.max_retries + 1)
|
|
242
|
+
if last_exception:
|
|
243
|
+
raise last_exception
|
|
244
|
+
else:
|
|
245
|
+
raise RuntimeError("OAuth token request failed after all retries")
|