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
5 changes: 5 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,11 @@
CatalogDependentEntitiesResponse,
CatalogEntityIdentifier,
)
from gooddata_sdk.catalog.workspace.entity_model.resolved_llm import (
CatalogResolvedLlmModel,
CatalogResolvedLlmProvider,
CatalogResolvedLlms,
)
from gooddata_sdk.catalog.workspace.entity_model.user_data_filter import (
CatalogUserDataFilter,
CatalogUserDataFilterAttributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
CatalogDependentEntitiesRequest,
CatalogDependentEntitiesResponse,
)
from gooddata_sdk.catalog.workspace.entity_model.resolved_llm import (
CatalogResolvedLlms,
)
from gooddata_sdk.catalog.workspace.model_container import CatalogWorkspaceContent
from gooddata_sdk.client import GoodDataApiClient
from gooddata_sdk.compute.model.attribute import Attribute
Expand Down Expand Up @@ -685,3 +688,21 @@ 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) -> 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 (str):
Workspace identification string e.g. "demo"

Returns:
CatalogResolvedLlms:
Active LLM configuration for the workspace.
"""
response = self._actions_api.resolve_llm_providers(workspace_id, _check_return_type=False)
return CatalogResolvedLlms.from_api(response)
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# (C) 2026 GoodData Corporation
from __future__ import annotations

from typing import Any

import attrs
from gooddata_api_client.model.llm_model import LlmModel
from gooddata_api_client.model.resolved_llm_provider import ResolvedLlmProvider
from gooddata_api_client.model.resolved_llms import ResolvedLlms

from gooddata_sdk.catalog.base import Base


@attrs.define(kw_only=True)
class CatalogResolvedLlmModel(Base):
"""A single LLM model associated with a resolved LLM provider."""

id: str
family: str

@staticmethod
def client_class() -> type[LlmModel]:
return LlmModel


@attrs.define(kw_only=True)
class CatalogResolvedLlmProvider(Base):
"""Resolved LLM provider for a workspace.

Represents either a ResolvedLlmProvider (when ENABLE_LLM_ENDPOINT_REPLACEMENT
feature flag is enabled) or a ResolvedLlmEndpoint (legacy fallback).
When the legacy endpoint is returned, models will be an empty list.
"""

id: str
title: str
models: list[CatalogResolvedLlmModel] = attrs.field(factory=list)

@staticmethod
def client_class() -> type[ResolvedLlmProvider]:
return ResolvedLlmProvider

@classmethod
def from_api(cls, entity: Any) -> CatalogResolvedLlmProvider:
raw_models = getattr(entity, "models", None) or []
models = [CatalogResolvedLlmModel(id=m.id, family=m.family) for m in raw_models]
return cls(
id=entity.id,
title=entity.title,
models=models,
)


@attrs.define(kw_only=True)
class CatalogResolvedLlms(Base):
"""Response from the resolveLlmProviders workspace action endpoint.

Contains the active LLM configuration for a given workspace.
The data field is present when an active LLM configuration exists.
"""

data: CatalogResolvedLlmProvider | None = None

@staticmethod
def client_class() -> type[ResolvedLlms]:
return ResolvedLlms

@classmethod
def from_api(cls, entity: Any) -> CatalogResolvedLlms:
raw_data = getattr(entity, "data", None)
if raw_data is None:
return cls(data=None)
return cls(data=CatalogResolvedLlmProvider.from_api(raw_data))
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# (C) 2026 GoodData Corporation
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/actions/workspaces/demo/ai/resolveLlmProviders
response:
body:
string:
data:
id: openai-provider-id
models:
- family: OPENAI
id: gpt-4o
title: OpenAI Provider
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
CatalogDependsOn,
CatalogDependsOnDateFilter,
CatalogEntityIdentifier,
CatalogResolvedLlmModel,
CatalogResolvedLlmProvider,
CatalogResolvedLlms,
CatalogValidateByItem,
CatalogWorkspace,
DataSourceValidator,
Expand Down Expand Up @@ -502,3 +505,67 @@ def test_export_definition_analytics_layout(test_config):
assert deep_eq(analytics_o.analytics.export_definitions, analytics_e.analytics.export_definitions)
finally:
safe_delete(_refresh_workspaces, sdk)


def test_resolve_llm_providers_with_provider_response():
"""Unit test: resolve_llm_providers correctly deserializes a ResolvedLlmProvider response."""
mock_model = MagicMock()
mock_model.id = "gpt-4o"
mock_model.family = "GPT"

mock_data = MagicMock()
mock_data.id = "openai-provider"
mock_data.title = "OpenAI Provider"
mock_data.models = [mock_model]

mock_response = MagicMock()
mock_response.data = mock_data

result = CatalogResolvedLlms.from_api(mock_response)

assert isinstance(result, CatalogResolvedLlms)
assert isinstance(result.data, CatalogResolvedLlmProvider)
assert result.data.id == "openai-provider"
assert result.data.title == "OpenAI Provider"
assert len(result.data.models) == 1
assert isinstance(result.data.models[0], CatalogResolvedLlmModel)
assert result.data.models[0].id == "gpt-4o"
assert result.data.models[0].family == "GPT"


def test_resolve_llm_providers_with_no_data():
"""Unit test: resolve_llm_providers correctly handles a response with no data."""
mock_response = MagicMock()
mock_response.data = None

result = CatalogResolvedLlms.from_api(mock_response)

assert isinstance(result, CatalogResolvedLlms)
assert result.data is None


def test_resolve_llm_providers_legacy_endpoint_fallback():
"""Unit test: resolve_llm_providers handles legacy endpoint fallback (no models)."""
mock_data = MagicMock()
mock_data.id = "legacy-endpoint"
mock_data.title = "Legacy LLM Endpoint"
mock_data.models = None

mock_response = MagicMock()
mock_response.data = mock_data

result = CatalogResolvedLlms.from_api(mock_response)

assert isinstance(result, CatalogResolvedLlms)
assert isinstance(result.data, CatalogResolvedLlmProvider)
assert result.data.id == "legacy-endpoint"
assert result.data.title == "Legacy LLM Endpoint"
assert result.data.models == []


@gd_vcr.use_cassette(str(_fixtures_dir / "demo_resolve_llm_providers.yaml"))
def test_resolve_llm_providers_integration(test_config):
"""Integration test: resolve_llm_providers calls the API and returns a valid response."""
sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"])
result = sdk.catalog_workspace_content.resolve_llm_providers(test_config["workspace"])
assert isinstance(result, CatalogResolvedLlms)
Loading