Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
CatalogRsaSpecification,
)
from gooddata_sdk.catalog.organization.entity_model.llm_provider import (
CatalogAnthropicApiKeyAuth,
CatalogAnthropicProviderConfig,
CatalogAwsBedrockProviderConfig,
CatalogAzureFoundryApiKeyAuth,
CatalogAzureFoundryProviderConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ def client_class() -> type[AzureFoundryProviderAuth]:

CatalogAzureFoundryAuth = Union[CatalogAzureFoundryApiKeyAuth]

# --- Anthropic auth ---


@define(kw_only=True)
class CatalogAnthropicApiKeyAuth(Base):
"""API key authentication for the Anthropic provider."""

api_key: str | None = None
type: str = "API_KEY"

@staticmethod
def client_class() -> type[OpenAiProviderAuth]:
# Stand-in: AnthropicProviderAuth not yet present in the generated client.
return OpenAiProviderAuth


CatalogAnthropicAuth = Union[CatalogAnthropicApiKeyAuth]

# --- Provider config types ---


Expand Down Expand Up @@ -118,10 +136,25 @@ def client_class() -> type[AzureFoundryProviderConfig]:
return AzureFoundryProviderConfig


@define(kw_only=True)
class CatalogAnthropicProviderConfig(Base):
"""Anthropic provider configuration."""

auth: CatalogAnthropicAuth | None = None
base_url: str | None = None
type: str = "ANTHROPIC"

@staticmethod
def client_class() -> type[OpenAIProviderConfig]:
# Stand-in: AnthropicProviderConfig not yet present in the generated client.
return OpenAIProviderConfig


CatalogLlmProviderConfig = Union[
CatalogOpenAiProviderConfig,
CatalogAwsBedrockProviderConfig,
CatalogAzureFoundryProviderConfig,
CatalogAnthropicProviderConfig,
]


Expand Down Expand Up @@ -157,6 +190,35 @@ def _azure_foundry_auth_from_api(data: dict[str, Any]) -> CatalogAzureFoundryAut
raise ValueError(f"Unknown Azure Foundry auth type: {auth_type}")


def _anthropic_auth_from_api(data: dict[str, Any]) -> CatalogAnthropicAuth:
auth_type = safeget(data, ["type"]) or "API_KEY"
if auth_type == "API_KEY":
return CatalogAnthropicApiKeyAuth(
api_key="", # Credentials are not returned for security reasons
type=auth_type,
)
raise ValueError(f"Unknown Anthropic auth type: {auth_type}")


def _anthropic_config_to_camel_dict(config: CatalogAnthropicProviderConfig) -> dict[str, Any]:
"""Convert CatalogAnthropicProviderConfig to a camelCase dict for direct _data_store injection.

The generated API client's JsonApiLlmProviderInAttributesProviderConfig oneOf does not yet
include AnthropicProviderConfig. We bypass schema validation by storing the config as a
plain camelCase dict in _data_store; model_to_dict(serialize=True) serialises it correctly.
"""
result: dict[str, Any] = {"type": config.type}
if config.base_url is not None:
result["baseUrl"] = config.base_url
if config.auth is not None:
auth = config.auth
auth_dict: dict[str, Any] = {"type": auth.type}
if isinstance(auth, CatalogAnthropicApiKeyAuth) and auth.api_key is not None:
auth_dict["apiKey"] = auth.api_key
result["auth"] = auth_dict
return result


def _provider_config_from_api(data: dict[str, Any]) -> CatalogLlmProviderConfig:
provider_type = safeget(data, ["type"]) or "OPENAI"
auth_data = safeget(data, ["auth"])
Expand All @@ -173,6 +235,12 @@ def _provider_config_from_api(data: dict[str, Any]) -> CatalogLlmProviderConfig:
endpoint=safeget(data, ["endpoint"]),
)

if provider_type == "ANTHROPIC":
return CatalogAnthropicProviderConfig(
auth=_anthropic_auth_from_api(auth_data) if auth_data is not None else None,
base_url=safeget(data, ["baseUrl"]),
)

# Default: OpenAI
return CatalogOpenAiProviderConfig(
auth=_openai_auth_from_api(auth_data) if auth_data is not None else None,
Expand All @@ -192,6 +260,30 @@ class CatalogLlmProviderDocument(Base):
def client_class() -> type[JsonApiLlmProviderInDocument]:
return JsonApiLlmProviderInDocument

def to_api(self) -> JsonApiLlmProviderInDocument:
"""Build the API model with special handling for Anthropic provider config.

The generated API client's JsonApiLlmProviderInAttributesProviderConfig oneOf schema
does not yet include AnthropicProviderConfig, so the normal Base.to_api() / from_dict()
path raises ApiValueError for type='ANTHROPIC'. When the provider config is Anthropic,
we build the document without providerConfig (it is optional in the API), then inject
the config as a raw camelCase dict directly into _data_store to bypass schema validation.
"""
provider_config = (
self.data.attributes.provider_config if self.data.attributes is not None else None
)
if isinstance(provider_config, CatalogAnthropicProviderConfig):
snake_dict = self._get_snake_dict()
# Remove provider_config to avoid the oneOf validation failure in the generated client
snake_dict["data"]["attributes"].pop("provider_config", None)
api_doc = self.client_class().from_dict(snake_dict, camel_case=False)
# Inject provider_config directly as a raw camelCase dict, bypassing oneOf schema
api_doc.data.attributes._data_store["provider_config"] = (
_anthropic_config_to_camel_dict(provider_config)
)
return api_doc
return super().to_api()


@define(kw_only=True)
class CatalogLlmProviderPatchDocument(Base):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@

from gooddata_api_client.exceptions import NotFoundException
from gooddata_sdk import (
CatalogAnthropicApiKeyAuth,
CatalogAnthropicProviderConfig,
CatalogCspDirective,
CatalogDeclarativeNotificationChannel,
CatalogJwk,
CatalogLlmProvider,
CatalogLlmProviderModel,
CatalogOrganization,
CatalogOrganizationSetting,
CatalogRsaSpecification,
Expand Down Expand Up @@ -334,6 +338,29 @@ def test_layout_notification_channels(test_config, snapshot_notification_channel
# snapshot_notification_channels fixture restores original state in teardown


@gd_vcr.use_cassette(str(_fixtures_dir / "test_anthropic_llm_provider.yaml"))
def test_create_anthropic_llm_provider(test_config):
sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"])
provider_id = "test-anthropic-provider"
new_provider = CatalogLlmProvider.init(
id=provider_id,
models=[CatalogLlmProviderModel(id="claude-3-5-sonnet-20241022", family="ANTHROPIC")],
provider_config=CatalogAnthropicProviderConfig(
auth=CatalogAnthropicApiKeyAuth(api_key="sk-ant-test-key"),
base_url="https://api.anthropic.com",
),
name="Test Anthropic Provider",
)
try:
created = sdk.catalog_organization.create_llm_provider(new_provider)
assert created.id == provider_id
fetched = sdk.catalog_organization.get_llm_provider(provider_id)
assert fetched.id == provider_id
assert isinstance(fetched.attributes.provider_config, CatalogAnthropicProviderConfig)
finally:
safe_delete(sdk.catalog_organization.delete_llm_provider, provider_id)


#
# The following tests are commented out as they require the organization to have the FEDERATED_IDENTITY_MANAGEMENT
# entitlement enabled which cannot be done via SDK and must be done by GoodData support.
Expand Down
Loading