Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -561,3 +565,53 @@ 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
Loading