From 5e58f81d2686c4f94cc21711c5e5cfd6d7b20daf Mon Sep 17 00:00:00 2001 From: Auto Impl Date: Wed, 18 Mar 2026 09:54:00 +0000 Subject: [PATCH 1/2] feat(gooddata-sdk): [AUTO] Add CatalogResolvedLlmProvider models and resolve_llm_providers service method Wraps the new resolveLlmProviders action endpoint in the Python SDK: - New entity model classes: CatalogResolvedLlm, CatalogResolvedLlmModel, CatalogResolvedLlmProvider, CatalogResolvedLlms - New service method CatalogOrganizationService.resolve_llm_providers(workspace_id) - All new classes exported via gooddata_sdk.__init__ - Unit tests for the service method using mocks Co-Authored-By: Claude Sonnet 4.6 --- .../gooddata-sdk/src/gooddata_sdk/__init__.py | 6 ++ .../entity_model/resolved_llm_provider.py | 54 ++++++++++++++++++ .../catalog/organization/service.py | 17 ++++++ .../catalog/test_catalog_organization.py | 56 +++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/resolved_llm_provider.py diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index d268f9ebf..c7303d848 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -123,6 +123,12 @@ CatalogOpenAiApiKeyAuth, CatalogOpenAiProviderConfig, ) +from gooddata_sdk.catalog.organization.entity_model.resolved_llm_provider import ( + CatalogResolvedLlm, + CatalogResolvedLlmModel, + CatalogResolvedLlmProvider, + CatalogResolvedLlms, +) from gooddata_sdk.catalog.organization.entity_model.organization import CatalogOrganization from gooddata_sdk.catalog.organization.entity_model.setting import CatalogOrganizationSetting from gooddata_sdk.catalog.organization.layout.export_template import ( diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/resolved_llm_provider.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/resolved_llm_provider.py new file mode 100644 index 000000000..9c78929c4 --- /dev/null +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/resolved_llm_provider.py @@ -0,0 +1,54 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from typing import Any + +from attr import define + + +@define(kw_only=True) +class CatalogResolvedLlmModel: + """A resolved LLM model with id and family.""" + + id: str + family: str + + +@define(kw_only=True) +class CatalogResolvedLlm: + """Base resolved LLM with id and title.""" + + id: str + title: str + + +@define(kw_only=True) +class CatalogResolvedLlmProvider: + """A resolved LLM provider with associated models.""" + + id: str + title: str + models: list[CatalogResolvedLlmModel] + + @classmethod + def from_api_model(cls, obj: Any) -> CatalogResolvedLlmProvider: + raw_models = getattr(obj, "models", None) or [] + models = [CatalogResolvedLlmModel(id=m.id, family=m.family) for m in raw_models] + return cls(id=obj.id, title=obj.title, models=models) + + +@define(kw_only=True) +class CatalogResolvedLlms: + """Wrapper for the resolved LLMs response.""" + + data: CatalogResolvedLlmProvider | None = None + + @classmethod + def from_api_model(cls, obj: Any) -> CatalogResolvedLlms: + raw_data = getattr(obj, "data", None) + if raw_data is None: + return cls(data=None) + # Discriminate by presence of models field — provider has models, endpoint does not + if getattr(raw_data, "models", None) is not None: + return cls(data=CatalogResolvedLlmProvider.from_api_model(raw_data)) + return cls(data=None) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py index cbdd8bbf3..e601760a2 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py @@ -29,6 +29,7 @@ CatalogLlmProviderPatch, CatalogLlmProviderPatchDocument, ) +from gooddata_sdk.catalog.organization.entity_model.resolved_llm_provider import CatalogResolvedLlms from gooddata_sdk.catalog.organization.entity_model.setting import CatalogOrganizationSetting from gooddata_sdk.catalog.organization.layout.identity_provider import CatalogDeclarativeIdentityProvider from gooddata_sdk.catalog.organization.layout.notification_channel import CatalogDeclarativeNotificationChannel @@ -584,6 +585,22 @@ def delete_llm_provider(self, id: str) -> None: """ self._entities_api.delete_entity_llm_providers(id, _check_return_type=False) + def resolve_llm_providers(self, workspace_id: str) -> CatalogResolvedLlms: + """Resolve the active LLM configuration for a workspace. + + When the ENABLE_LLM_ENDPOINT_REPLACEMENT feature flag is enabled, returns LLM + Providers with their associated models. Otherwise, falls back to the legacy + LLM Endpoints. + + Args: + workspace_id: Workspace identifier + + Returns: + CatalogResolvedLlms: Resolved LLMs containing the active provider or endpoint. + """ + response = self._actions_api.resolve_llm_providers(workspace_id, _check_return_type=False) + return CatalogResolvedLlms.from_api_model(response) + # Layout APIs def get_declarative_notification_channels(self) -> list[CatalogDeclarativeNotificationChannel]: diff --git a/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py b/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py index fc1e0fb99..ad27e7d92 100644 --- a/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py +++ b/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py @@ -2,6 +2,7 @@ from __future__ import annotations from pathlib import Path +from unittest.mock import MagicMock from gooddata_api_client.exceptions import NotFoundException from gooddata_sdk import ( @@ -11,9 +12,12 @@ CatalogOrganization, CatalogOrganizationSetting, CatalogRsaSpecification, + CatalogResolvedLlmProvider, + CatalogResolvedLlms, CatalogWebhook, GoodDataSdk, ) +from gooddata_sdk.catalog.organization.service import CatalogOrganizationService from tests_support.vcrpy_utils import get_vcr from .conftest import safe_delete @@ -561,3 +565,55 @@ def test_layout_notification_channels(test_config, snapshot_notification_channel # sdk.catalog_organization.put_declarative_identity_providers([]) # idps = sdk.catalog_organization.get_declarative_identity_providers() # assert len(idps) == 0 + + +def test_resolve_llm_providers_returns_provider(): + """Unit test: resolve_llm_providers wraps API response into CatalogResolvedLlms.""" + mock_client = MagicMock() + + # Build a fake LlmModel-like object + mock_model = MagicMock() + mock_model.id = "gpt-4o" + mock_model.family = "OPENAI" + + # Build a fake ResolvedLlmProvider-like API response object + mock_data = MagicMock() + mock_data.id = "my-provider" + mock_data.title = "My Provider" + mock_data.models = [mock_model] + + # Build a fake ResolvedLlms-like API response object + mock_response = MagicMock() + mock_response.data = mock_data + + mock_client.actions_api.resolve_llm_providers.return_value = mock_response + + service = CatalogOrganizationService(mock_client) + result = service.resolve_llm_providers("demo-workspace") + + mock_client.actions_api.resolve_llm_providers.assert_called_once_with( + "demo-workspace", _check_return_type=False + ) + assert isinstance(result, CatalogResolvedLlms) + assert isinstance(result.data, CatalogResolvedLlmProvider) + assert result.data.id == "my-provider" + assert result.data.title == "My Provider" + assert len(result.data.models) == 1 + assert result.data.models[0].id == "gpt-4o" + assert result.data.models[0].family == "OPENAI" + + +def test_resolve_llm_providers_returns_none_data(): + """Unit test: resolve_llm_providers returns CatalogResolvedLlms with data=None when no provider.""" + mock_client = MagicMock() + + mock_response = MagicMock() + mock_response.data = None + + mock_client.actions_api.resolve_llm_providers.return_value = mock_response + + service = CatalogOrganizationService(mock_client) + result = service.resolve_llm_providers("demo-workspace") + + assert isinstance(result, CatalogResolvedLlms) + assert result.data is None From 21ca47ab1f81caef3216e11851844116b84863dc Mon Sep 17 00:00:00 2001 From: Auto Impl Date: Wed, 18 Mar 2026 09:54:25 +0000 Subject: [PATCH 2/2] chore: [AUTO] apply ruff formatting --- .../gooddata-sdk/tests/catalog/test_catalog_organization.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py b/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py index ad27e7d92..17e231e43 100644 --- a/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py +++ b/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py @@ -591,9 +591,7 @@ def test_resolve_llm_providers_returns_provider(): service = CatalogOrganizationService(mock_client) result = service.resolve_llm_providers("demo-workspace") - mock_client.actions_api.resolve_llm_providers.assert_called_once_with( - "demo-workspace", _check_return_type=False - ) + mock_client.actions_api.resolve_llm_providers.assert_called_once_with("demo-workspace", _check_return_type=False) assert isinstance(result, CatalogResolvedLlms) assert isinstance(result.data, CatalogResolvedLlmProvider) assert result.data.id == "my-provider"