From 42c116ba24658910325f9c4813110df051f2f7a7 Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:33:05 +0000 Subject: [PATCH 1/2] feat(gooddata-sdk): [AUTO] Add LlmProvider entity (Bedrock/Azure/OpenAI) and deprecate LlmEndpoint --- .../gooddata-sdk/src/gooddata_sdk/__init__.py | 2 + .../organization/entity_model/llm_provider.py | 52 ++++++++++++ .../catalog/organization/service.py | 14 +++ .../catalog/workspace/content_service.py | 24 ++++++ .../catalog/test_catalog_organization.py | 85 +++++++++++++++++++ 5 files changed, 177 insertions(+) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index 77397b92d..c94988c28 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -115,6 +115,7 @@ CatalogAzureFoundryApiKeyAuth, CatalogAzureFoundryProviderConfig, CatalogBedrockAccessKeyAuth, + CatalogListLlmProviderModelsResponse, CatalogLlmProvider, CatalogLlmProviderDocument, CatalogLlmProviderModel, @@ -122,6 +123,7 @@ CatalogLlmProviderPatchDocument, CatalogOpenAiApiKeyAuth, CatalogOpenAiProviderConfig, + CatalogResolvedLlm, ) from gooddata_sdk.catalog.organization.entity_model.organization import CatalogOrganization from gooddata_sdk.catalog.organization.entity_model.setting import CatalogOrganizationSetting diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py index 51d089eb5..d39a09284 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py @@ -3,6 +3,7 @@ from typing import Any, Union +import attrs from attr import define from gooddata_api_client.model.aws_bedrock_provider_config import AwsBedrockProviderConfig from gooddata_api_client.model.azure_foundry_provider_auth import AzureFoundryProviderAuth @@ -337,3 +338,54 @@ class CatalogLlmProviderPatchAttributes(Base): @staticmethod def client_class() -> type[JsonApiLlmProviderInAttributes]: return JsonApiLlmProviderInAttributes + + +# --- Resolved LLM response types (read-only, from action API) --- + + +@attrs.define(kw_only=True) +class CatalogResolvedLlm: + """Represents a resolved LLM configuration (provider or legacy endpoint) for a workspace.""" + + id: str + title: str + models: list[CatalogLlmProviderModel] = attrs.field(factory=list) + + @classmethod + def from_api(cls, entity: Any) -> CatalogResolvedLlm: + raw_models = safeget(entity, ["models"]) or [] + return cls( + id=safeget(entity, ["id"]) or "", + title=safeget(entity, ["title"]) or "", + models=[ + CatalogLlmProviderModel( + id=safeget(m, ["id"]) or "", + family=safeget(m, ["family"]) or "", + ) + for m in raw_models + ], + ) + + +@attrs.define(kw_only=True) +class CatalogListLlmProviderModelsResponse: + """Response from listing available models for an LLM provider.""" + + success: bool + message: str + models: list[CatalogLlmProviderModel] = attrs.field(factory=list) + + @classmethod + def from_api(cls, entity: Any) -> CatalogListLlmProviderModelsResponse: + raw_models = safeget(entity, ["models"]) or [] + return cls( + success=safeget(entity, ["success"]) or False, + message=safeget(entity, ["message"]) or "", + models=[ + CatalogLlmProviderModel( + id=safeget(m, ["id"]) or "", + family=safeget(m, ["family"]) or "", + ) + for m in raw_models + ], + ) 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..35103dd10 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py @@ -24,6 +24,7 @@ from gooddata_sdk.catalog.organization.entity_model.identity_provider import CatalogIdentityProvider from gooddata_sdk.catalog.organization.entity_model.jwk import CatalogJwk, CatalogJwkDocument from gooddata_sdk.catalog.organization.entity_model.llm_provider import ( + CatalogListLlmProviderModelsResponse, CatalogLlmProvider, CatalogLlmProviderDocument, CatalogLlmProviderPatch, @@ -584,6 +585,19 @@ def delete_llm_provider(self, id: str) -> None: """ self._entities_api.delete_entity_llm_providers(id, _check_return_type=False) + def list_llm_provider_models_by_id(self, llm_provider_id: str) -> CatalogListLlmProviderModelsResponse: + """List available models for an existing LLM provider by its ID. + + Args: + llm_provider_id: LLM provider identifier + + Returns: + CatalogListLlmProviderModelsResponse: Response containing available models + and a success flag. + """ + response = self._actions_api.list_llm_provider_models_by_id(llm_provider_id, _check_return_type=False) + return CatalogListLlmProviderModelsResponse.from_api(response) + # Layout APIs def get_declarative_notification_channels(self) -> list[CatalogDeclarativeNotificationChannel]: diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py index 7be97bee2..f833f2485 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py @@ -13,6 +13,7 @@ from gooddata_sdk.catalog.data_source.validation.data_source import DataSourceValidator from gooddata_sdk.catalog.depends_on import CatalogDependsOn, CatalogDependsOnDateFilter from gooddata_sdk.catalog.filter_by import CatalogFilterBy +from gooddata_sdk.catalog.organization.entity_model.llm_provider import CatalogResolvedLlm from gooddata_sdk.catalog.types import ValidObjects from gooddata_sdk.catalog.validate_by_item import CatalogValidateByItem from gooddata_sdk.catalog.workspace.declarative_model.workspace.analytics_model.analytics_model import ( @@ -685,3 +686,26 @@ def get_label_elements( workspace_id, request, _check_return_type=False, **paging_params ) return [v["title"] for v in values["elements"]] + + def resolve_llm_providers(self, workspace_id: str) -> CatalogResolvedLlm | None: + """Resolve the active LLM configuration for a workspace. + + When the ENABLE_LLM_ENDPOINT_REPLACEMENT feature flag is enabled, returns + the active LLM provider with its associated models. Otherwise, falls back + to the legacy LLM endpoint response format. + + Args: + workspace_id (str): + Workspace identification string e.g. "demo". + + Returns: + CatalogResolvedLlm | None: + Resolved LLM configuration, or None if no active configuration exists. + """ + response = self._actions_api.resolve_llm_providers(workspace_id, _check_return_type=False) + if response is None: + return None + data = response.data if hasattr(response, "data") else None + if data is None: + return None + return CatalogResolvedLlm.from_api(data) diff --git a/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py b/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py index 53e88c566..1c7ab0a87 100644 --- a/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py +++ b/packages/gooddata-sdk/tests/catalog/test_catalog_organization.py @@ -3,13 +3,16 @@ from pathlib import Path +import pytest from gooddata_api_client.exceptions import NotFoundException from gooddata_sdk import ( CatalogCspDirective, CatalogDeclarativeNotificationChannel, CatalogJwk, + CatalogListLlmProviderModelsResponse, CatalogOrganization, CatalogOrganizationSetting, + CatalogResolvedLlm, CatalogRsaSpecification, CatalogWebhook, GoodDataSdk, @@ -563,3 +566,85 @@ 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 + + +# --------------------------------------------------------------------------- +# Unit tests for LLM provider response model classes (no cassettes needed) +# --------------------------------------------------------------------------- + + +def test_catalog_resolved_llm_from_api(): + """CatalogResolvedLlm.from_api deserializes a plain dict correctly.""" + data = { + "id": "my-provider", + "title": "My LLM Provider", + "models": [ + {"id": "gpt-4o", "family": "OPENAI"}, + {"id": "gpt-4-turbo", "family": "OPENAI"}, + ], + } + result = CatalogResolvedLlm.from_api(data) + assert result.id == "my-provider" + assert result.title == "My LLM Provider" + assert len(result.models) == 2 + assert result.models[0].id == "gpt-4o" + assert result.models[0].family == "OPENAI" + assert result.models[1].id == "gpt-4-turbo" + + +def test_catalog_resolved_llm_from_api_empty_models(): + """CatalogResolvedLlm.from_api handles missing models gracefully.""" + data = {"id": "provider-no-models", "title": "Provider"} + result = CatalogResolvedLlm.from_api(data) + assert result.id == "provider-no-models" + assert result.title == "Provider" + assert result.models == [] + + +def test_catalog_list_llm_provider_models_response_from_api(): + """CatalogListLlmProviderModelsResponse.from_api deserializes correctly.""" + data = { + "success": True, + "message": "Models listed successfully.", + "models": [ + {"id": "claude-3-5-sonnet-20241022", "family": "ANTHROPIC"}, + ], + } + result = CatalogListLlmProviderModelsResponse.from_api(data) + assert result.success is True + assert result.message == "Models listed successfully." + assert len(result.models) == 1 + assert result.models[0].id == "claude-3-5-sonnet-20241022" + assert result.models[0].family == "ANTHROPIC" + + +def test_catalog_list_llm_provider_models_response_failure(): + """CatalogListLlmProviderModelsResponse.from_api handles failure response.""" + data = { + "success": False, + "message": "Authentication failed.", + "models": [], + } + result = CatalogListLlmProviderModelsResponse.from_api(data) + assert result.success is False + assert result.message == "Authentication failed." + assert result.models == [] + + +# --------------------------------------------------------------------------- +# Integration tests for LLM provider CRUD and action methods +# (cassettes to be recorded against a live server) +# --------------------------------------------------------------------------- + + +@gd_vcr.use_cassette(str(_fixtures_dir / "list_llm_provider_models_by_id.yaml")) +def test_list_llm_provider_models_by_id(test_config): + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + providers = sdk.catalog_organization.list_llm_providers() + if not providers: + pytest.skip("No LLM providers configured — cannot test list_llm_provider_models_by_id") + provider_id = providers[0].id + response = sdk.catalog_organization.list_llm_provider_models_by_id(provider_id) + assert isinstance(response, CatalogListLlmProviderModelsResponse) + assert isinstance(response.success, bool) + assert isinstance(response.message, str) From 6b43ddab28b60fd31b4f152ddbd1cb3049c7749c Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:57:28 +0000 Subject: [PATCH 2/2] chore(cassettes): record cassettes for auto-sync tests --- .../list_llm_provider_models_by_id.yaml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 packages/gooddata-sdk/tests/catalog/fixtures/organization/list_llm_provider_models_by_id.yaml diff --git a/packages/gooddata-sdk/tests/catalog/fixtures/organization/list_llm_provider_models_by_id.yaml b/packages/gooddata-sdk/tests/catalog/fixtures/organization/list_llm_provider_models_by_id.yaml new file mode 100644 index 000000000..970470b2b --- /dev/null +++ b/packages/gooddata-sdk/tests/catalog/fixtures/organization/list_llm_provider_models_by_id.yaml @@ -0,0 +1,37 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: GET + uri: http://localhost:3000/api/v1/entities/llmProviders + response: + body: + string: + data: [] + links: + next: http://localhost:3000/api/v1/entities/llmProviders?page=1&size=20 + self: http://localhost:3000/api/v1/entities/llmProviders?page=0&size=20 + headers: + Content-Type: + - application/json + DATE: &id001 + - PLACEHOLDER + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: *id001 + status: + code: 200 + message: OK +version: 1