From 0fea4f90ef1863d27dc20341570b956aca143635 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 1 Jun 2026 12:57:14 +0530 Subject: [PATCH] fix(a2a): treat CP4D token expiration as absolute Unix time CP4D /authorize returns expiration as epoch seconds, not TTL. Compute remaining lifetime against wall clock so cached tokens refresh before expiry. Co-authored-by: Cursor --- .../providers/watsonx_orchestrate/handler.py | 13 ++++++++++++- .../test_watsonx_orchestrate_transformation.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/litellm/a2a_protocol/providers/watsonx_orchestrate/handler.py b/litellm/a2a_protocol/providers/watsonx_orchestrate/handler.py index 90aaf8300ef..038258a5bd6 100644 --- a/litellm/a2a_protocol/providers/watsonx_orchestrate/handler.py +++ b/litellm/a2a_protocol/providers/watsonx_orchestrate/handler.py @@ -43,6 +43,13 @@ class WatsonxOrchestrateHandler: material = f"{auth_mode}:{cp4d_host}:{username or ''}:{api_key}" return hashlib.sha256(material.encode()).hexdigest() + @staticmethod + def _cp4d_token_ttl_seconds(expiration: Any, now_wall: Optional[float] = None) -> int: + # CP4D returns expiration as absolute Unix epoch seconds, not a duration. + expires_at = int(expiration) + wall = now_wall if now_wall is not None else time.time() + return max(expires_at - int(wall), 0) + @staticmethod async def _get_bearer_token( cp4d_host: str, @@ -89,7 +96,11 @@ class WatsonxOrchestrateHandler: response.raise_for_status() payload = response.json() token = str(payload["token"]) - ttl_s = int(payload.get("expiration", 3600)) + expiration = payload.get("expiration") + if expiration is None: + ttl_s = 3600 + else: + ttl_s = WatsonxOrchestrateHandler._cp4d_token_ttl_seconds(expiration) expires_at = now + max(ttl_s - _TOKEN_CACHE_TTL_BUFFER_S, 60) _token_cache[cache_key] = (token, expires_at) diff --git a/tests/test_litellm/a2a_protocol/providers/watsonx_orchestrate/test_watsonx_orchestrate_transformation.py b/tests/test_litellm/a2a_protocol/providers/watsonx_orchestrate/test_watsonx_orchestrate_transformation.py index a4aa9972043..3e0375f7b90 100644 --- a/tests/test_litellm/a2a_protocol/providers/watsonx_orchestrate/test_watsonx_orchestrate_transformation.py +++ b/tests/test_litellm/a2a_protocol/providers/watsonx_orchestrate/test_watsonx_orchestrate_transformation.py @@ -6,6 +6,9 @@ import pytest sys.path.insert(0, os.path.abspath("../../../../..")) from litellm.a2a_protocol.providers.config_manager import A2AProviderConfigManager +from litellm.a2a_protocol.providers.watsonx_orchestrate.handler import ( + WatsonxOrchestrateHandler, +) from litellm.a2a_protocol.providers.watsonx_orchestrate.transformation import ( WatsonxOrchestrateTransformation, ) @@ -104,6 +107,14 @@ class TestWatsonxOrchestrateTransformation: ) +def test_cp4d_token_ttl_from_absolute_expiration(): + wall = 1_750_000_000.0 + assert ( + WatsonxOrchestrateHandler._cp4d_token_ttl_seconds(1_750_003_600, wall) == 3600 + ) + assert WatsonxOrchestrateHandler._cp4d_token_ttl_seconds(1_749_999_000, wall) == 0 + + def test_config_manager_returns_wxo_provider(): config = A2AProviderConfigManager.get_provider_config( custom_llm_provider="watsonx_orchestrate"