Skip to content

Commit 720b2a2

Browse files
Merge pull request #1635 from gooddata/snapshot-master-0dc62841-to-rel/dev
[bot] Merge master/0dc62841 into rel/dev
2 parents 0e18ae9 + 0dc6284 commit 720b2a2

4 files changed

Lines changed: 251 additions & 0 deletions

File tree

packages/gooddata-sdk/src/gooddata_sdk/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,11 @@
123123
CatalogLlmProvider,
124124
CatalogLlmProviderDocument,
125125
CatalogLlmProviderModel,
126+
CatalogLlmProviderModelsResult,
126127
CatalogLlmProviderPatch,
127128
CatalogLlmProviderPatchDocument,
129+
CatalogLlmProviderTestResult,
130+
CatalogModelTestResult,
128131
CatalogOpenAiApiKeyAuth,
129132
CatalogOpenAiProviderConfig,
130133
)

packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/llm_provider.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
from gooddata_api_client.model.json_api_llm_provider_in_document import JsonApiLlmProviderInDocument
1717
from gooddata_api_client.model.json_api_llm_provider_patch import JsonApiLlmProviderPatch
1818
from gooddata_api_client.model.json_api_llm_provider_patch_document import JsonApiLlmProviderPatchDocument
19+
from gooddata_api_client.model.list_llm_provider_models_response import ListLlmProviderModelsResponse
20+
from gooddata_api_client.model.model_test_result import ModelTestResult
1921
from gooddata_api_client.model.open_ai_provider_auth import OpenAiProviderAuth
2022
from gooddata_api_client.model.open_ai_provider_config import OpenAIProviderConfig
23+
from gooddata_api_client.model.test_llm_provider_response import TestLlmProviderResponse
2124

2225
from gooddata_sdk.catalog.base import Base
2326
from gooddata_sdk.utils import safeget
@@ -337,3 +340,45 @@ class CatalogLlmProviderPatchAttributes(Base):
337340
@staticmethod
338341
def client_class() -> type[JsonApiLlmProviderInAttributes]:
339342
return JsonApiLlmProviderInAttributes
343+
344+
345+
# --- Action result types ---
346+
347+
348+
@define(kw_only=True)
349+
class CatalogModelTestResult(Base):
350+
"""Result of testing a single model on an LLM provider."""
351+
352+
model_id: str
353+
successful: bool
354+
message: str | None = None
355+
356+
@staticmethod
357+
def client_class() -> type[ModelTestResult]:
358+
return ModelTestResult
359+
360+
361+
@define(kw_only=True)
362+
class CatalogLlmProviderTestResult(Base):
363+
"""Result of testing connectivity to an LLM provider."""
364+
365+
provider_reachable: bool
366+
provider_message: str | None = None
367+
model_results: list[CatalogModelTestResult] | None = None
368+
369+
@staticmethod
370+
def client_class() -> type[TestLlmProviderResponse]:
371+
return TestLlmProviderResponse
372+
373+
374+
@define(kw_only=True)
375+
class CatalogLlmProviderModelsResult(Base):
376+
"""Result of listing the models available for an LLM provider."""
377+
378+
success: bool
379+
message: str | None = None
380+
models: list[CatalogLlmProviderModel] | None = None
381+
382+
@staticmethod
383+
def client_class() -> type[ListLlmProviderModelsResponse]:
384+
return ListLlmProviderModelsResponse

packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/service.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
from gooddata_api_client.model.json_api_organization_patch import JsonApiOrganizationPatch
1717
from gooddata_api_client.model.json_api_organization_patch_document import JsonApiOrganizationPatchDocument
1818
from gooddata_api_client.model.json_api_organization_setting_in_document import JsonApiOrganizationSettingInDocument
19+
from gooddata_api_client.model.list_llm_provider_models_request import ListLlmProviderModelsRequest
1920
from gooddata_api_client.model.switch_identity_provider_request import SwitchIdentityProviderRequest
21+
from gooddata_api_client.model.test_llm_provider_by_id_request import TestLlmProviderByIdRequest
22+
from gooddata_api_client.model.test_llm_provider_definition_request import TestLlmProviderDefinitionRequest
2023

2124
from gooddata_sdk import CatalogDeclarativeExportTemplate, CatalogExportTemplate
2225
from gooddata_sdk.catalog.catalog_service_base import CatalogServiceBase
@@ -25,9 +28,13 @@
2528
from gooddata_sdk.catalog.organization.entity_model.jwk import CatalogJwk, CatalogJwkDocument
2629
from gooddata_sdk.catalog.organization.entity_model.llm_provider import (
2730
CatalogLlmProvider,
31+
CatalogLlmProviderConfig,
2832
CatalogLlmProviderDocument,
33+
CatalogLlmProviderModel,
34+
CatalogLlmProviderModelsResult,
2935
CatalogLlmProviderPatch,
3036
CatalogLlmProviderPatchDocument,
37+
CatalogLlmProviderTestResult,
3138
)
3239
from gooddata_sdk.catalog.organization.entity_model.setting import CatalogOrganizationSetting
3340
from gooddata_sdk.catalog.organization.layout.identity_provider import CatalogDeclarativeIdentityProvider
@@ -628,6 +635,77 @@ def delete_llm_provider(self, id: str) -> None:
628635
"""
629636
self._entities_api.delete_entity_llm_providers(id, _check_return_type=False)
630637

638+
def test_llm_provider(
639+
self,
640+
provider_config: CatalogLlmProviderConfig,
641+
models: list[CatalogLlmProviderModel] | None = None,
642+
) -> CatalogLlmProviderTestResult:
643+
"""Test connectivity to an unsaved LLM provider configuration.
644+
645+
Args:
646+
provider_config: Provider configuration to test (OpenAI, AWS Bedrock, or Azure Foundry).
647+
models: Optional list of models to test against the provider.
648+
649+
Returns:
650+
CatalogLlmProviderTestResult: Reachability and per-model test results.
651+
"""
652+
payload: dict = {"providerConfig": provider_config.to_api().to_dict()}
653+
if models is not None:
654+
payload["models"] = [m.to_api().to_dict() for m in models]
655+
request = TestLlmProviderDefinitionRequest.from_dict(payload)
656+
response = self._actions_api.test_llm_provider(request)
657+
return CatalogLlmProviderTestResult.from_api(response)
658+
659+
def test_llm_provider_by_id(
660+
self,
661+
id: str,
662+
models: list[CatalogLlmProviderModel] | None = None,
663+
) -> CatalogLlmProviderTestResult:
664+
"""Test connectivity to a saved LLM provider by its identifier.
665+
666+
Args:
667+
id: LLM provider identifier.
668+
models: Optional list of models to test (overrides the provider's models).
669+
670+
Returns:
671+
CatalogLlmProviderTestResult: Reachability and per-model test results.
672+
"""
673+
kwargs: dict = {}
674+
if models is not None:
675+
kwargs["test_llm_provider_by_id_request"] = TestLlmProviderByIdRequest.from_dict(
676+
{"models": [m.to_api().to_dict() for m in models]}
677+
)
678+
response = self._actions_api.test_llm_provider_by_id(id, **kwargs)
679+
return CatalogLlmProviderTestResult.from_api(response)
680+
681+
def list_llm_provider_models(
682+
self,
683+
provider_config: CatalogLlmProviderConfig,
684+
) -> CatalogLlmProviderModelsResult:
685+
"""List the models available for an unsaved LLM provider configuration.
686+
687+
Args:
688+
provider_config: Provider configuration to query.
689+
690+
Returns:
691+
CatalogLlmProviderModelsResult: Available models and query status.
692+
"""
693+
request = ListLlmProviderModelsRequest.from_dict({"providerConfig": provider_config.to_api().to_dict()})
694+
response = self._actions_api.list_llm_provider_models(request)
695+
return CatalogLlmProviderModelsResult.from_api(response)
696+
697+
def list_llm_provider_models_by_id(self, id: str) -> CatalogLlmProviderModelsResult:
698+
"""List the models available for a saved LLM provider by its identifier.
699+
700+
Args:
701+
id: LLM provider identifier.
702+
703+
Returns:
704+
CatalogLlmProviderModelsResult: Available models and query status.
705+
"""
706+
response = self._actions_api.list_llm_provider_models_by_id(id)
707+
return CatalogLlmProviderModelsResult.from_api(response)
708+
631709
# Layout APIs
632710

633711
def get_declarative_notification_channels(self) -> list[CatalogDeclarativeNotificationChannel]:
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# (C) 2026 GoodData Corporation
2+
from __future__ import annotations
3+
4+
from unittest.mock import MagicMock
5+
6+
from gooddata_api_client.model.list_llm_provider_models_response import ListLlmProviderModelsResponse
7+
from gooddata_api_client.model.llm_model import LlmModel
8+
from gooddata_api_client.model.model_test_result import ModelTestResult
9+
from gooddata_api_client.model.test_llm_provider_response import TestLlmProviderResponse
10+
from gooddata_sdk import (
11+
CatalogLlmProviderModel,
12+
CatalogLlmProviderModelsResult,
13+
CatalogLlmProviderTestResult,
14+
CatalogModelTestResult,
15+
CatalogOpenAiApiKeyAuth,
16+
CatalogOpenAiProviderConfig,
17+
)
18+
from gooddata_sdk.catalog.organization.service import CatalogOrganizationService
19+
20+
21+
def test_provider_test_result_from_api():
22+
api_response = TestLlmProviderResponse(
23+
provider_reachable=True,
24+
provider_message="ok",
25+
model_results=[ModelTestResult(model_id="gpt-4o", successful=True, message="ok")],
26+
)
27+
28+
result = CatalogLlmProviderTestResult.from_api(api_response)
29+
30+
assert result.provider_reachable is True
31+
assert result.provider_message == "ok"
32+
assert len(result.model_results) == 1
33+
assert isinstance(result.model_results[0], CatalogModelTestResult)
34+
assert result.model_results[0].model_id == "gpt-4o"
35+
assert result.model_results[0].successful is True
36+
assert result.model_results[0].message == "ok"
37+
38+
39+
def test_provider_models_result_from_api():
40+
api_response = ListLlmProviderModelsResponse(
41+
success=True,
42+
message="ok",
43+
models=[LlmModel(id="gpt-4o", family="OPENAI")],
44+
)
45+
46+
result = CatalogLlmProviderModelsResult.from_api(api_response)
47+
48+
assert result.success is True
49+
assert result.message == "ok"
50+
assert len(result.models) == 1
51+
assert isinstance(result.models[0], CatalogLlmProviderModel)
52+
assert result.models[0].id == "gpt-4o"
53+
assert result.models[0].family == "OPENAI"
54+
55+
56+
def _openai_config():
57+
return CatalogOpenAiProviderConfig(
58+
auth=CatalogOpenAiApiKeyAuth(api_key="secret"),
59+
base_url="https://api.openai.com/v1",
60+
)
61+
62+
63+
def test_test_llm_provider_maps_response():
64+
service = CatalogOrganizationService.__new__(CatalogOrganizationService)
65+
service._actions_api = MagicMock()
66+
service._actions_api.test_llm_provider.return_value = TestLlmProviderResponse(
67+
provider_reachable=False,
68+
provider_message="bad key",
69+
model_results=[],
70+
)
71+
72+
result = service.test_llm_provider(_openai_config())
73+
74+
assert result.provider_reachable is False
75+
assert result.provider_message == "bad key"
76+
# the request body passed to the client carries the provider config discriminator
77+
sent = service._actions_api.test_llm_provider.call_args.args[0]
78+
assert sent["provider_config"]["type"] == "OPENAI"
79+
80+
81+
def test_test_llm_provider_by_id_calls_client_with_id():
82+
service = CatalogOrganizationService.__new__(CatalogOrganizationService)
83+
service._actions_api = MagicMock()
84+
service._actions_api.test_llm_provider_by_id.return_value = TestLlmProviderResponse(
85+
provider_reachable=True,
86+
provider_message="ok",
87+
model_results=[],
88+
)
89+
90+
result = service.test_llm_provider_by_id("my-provider")
91+
92+
assert result.provider_reachable is True
93+
assert service._actions_api.test_llm_provider_by_id.call_args.args[0] == "my-provider"
94+
95+
96+
def test_list_llm_provider_models_maps_response():
97+
service = CatalogOrganizationService.__new__(CatalogOrganizationService)
98+
service._actions_api = MagicMock()
99+
service._actions_api.list_llm_provider_models.return_value = ListLlmProviderModelsResponse(
100+
success=True,
101+
message="ok",
102+
models=[LlmModel(id="gpt-4o", family="OPENAI")],
103+
)
104+
105+
result = service.list_llm_provider_models(_openai_config())
106+
107+
assert result.success is True
108+
assert result.models[0].id == "gpt-4o"
109+
sent = service._actions_api.list_llm_provider_models.call_args.args[0]
110+
assert sent["provider_config"]["type"] == "OPENAI"
111+
112+
113+
def test_list_llm_provider_models_by_id_calls_client_with_id():
114+
service = CatalogOrganizationService.__new__(CatalogOrganizationService)
115+
service._actions_api = MagicMock()
116+
service._actions_api.list_llm_provider_models_by_id.return_value = ListLlmProviderModelsResponse(
117+
success=True,
118+
message="ok",
119+
models=[],
120+
)
121+
122+
result = service.list_llm_provider_models_by_id("my-provider")
123+
124+
assert result.success is True
125+
assert service._actions_api.list_llm_provider_models_by_id.call_args.args[0] == "my-provider"

0 commit comments

Comments
 (0)