diff --git a/gateway/gateway-controller/default-llm-provider-templates/anthropic-template.yaml b/gateway/gateway-controller/default-llm-provider-templates/anthropic-template.yaml index 6744eb9d83..0b4f38524f 100644 --- a/gateway/gateway-controller/default-llm-provider-templates/anthropic-template.yaml +++ b/gateway/gateway-controller/default-llm-provider-templates/anthropic-template.yaml @@ -22,7 +22,7 @@ metadata: name: anthropic spec: displayName: Anthropic - groupId: anthropic + groupId: wso2-anthropic managedBy: wso2 version: v1.0 promptTokens: diff --git a/gateway/gateway-controller/default-llm-provider-templates/awsbedrock-template.yaml b/gateway/gateway-controller/default-llm-provider-templates/awsbedrock-template.yaml index 5b8968f9d5..f2b7bc5de9 100644 --- a/gateway/gateway-controller/default-llm-provider-templates/awsbedrock-template.yaml +++ b/gateway/gateway-controller/default-llm-provider-templates/awsbedrock-template.yaml @@ -22,7 +22,7 @@ metadata: name: awsbedrock spec: displayName: AWS Bedrock - groupId: awsbedrock + groupId: wso2-awsbedrock managedBy: wso2 version: v1.0 promptTokens: diff --git a/gateway/gateway-controller/default-llm-provider-templates/azureaifoundry-template.yaml b/gateway/gateway-controller/default-llm-provider-templates/azureaifoundry-template.yaml index a40afb1b28..42820443ff 100644 --- a/gateway/gateway-controller/default-llm-provider-templates/azureaifoundry-template.yaml +++ b/gateway/gateway-controller/default-llm-provider-templates/azureaifoundry-template.yaml @@ -22,7 +22,7 @@ metadata: name: azureai-foundry spec: displayName: Azure AI Foundry - groupId: azureai-foundry + groupId: wso2-azureai-foundry managedBy: wso2 version: v1.0 promptTokens: diff --git a/gateway/gateway-controller/default-llm-provider-templates/azureopenai-template.yaml b/gateway/gateway-controller/default-llm-provider-templates/azureopenai-template.yaml index a3da0b060b..29f899c331 100644 --- a/gateway/gateway-controller/default-llm-provider-templates/azureopenai-template.yaml +++ b/gateway/gateway-controller/default-llm-provider-templates/azureopenai-template.yaml @@ -22,7 +22,7 @@ metadata: name: azure-openai spec: displayName: Azure OpenAI - groupId: azure-openai + groupId: wso2-azure-openai managedBy: wso2 version: v1.0 promptTokens: diff --git a/gateway/gateway-controller/default-llm-provider-templates/gemini-template.yaml b/gateway/gateway-controller/default-llm-provider-templates/gemini-template.yaml index cafd577ddf..79a5a8be94 100644 --- a/gateway/gateway-controller/default-llm-provider-templates/gemini-template.yaml +++ b/gateway/gateway-controller/default-llm-provider-templates/gemini-template.yaml @@ -22,7 +22,7 @@ metadata: name: gemini spec: displayName: Gemini - groupId: gemini + groupId: wso2-gemini managedBy: wso2 version: v1.0 promptTokens: diff --git a/gateway/gateway-controller/default-llm-provider-templates/mistral-template.yaml b/gateway/gateway-controller/default-llm-provider-templates/mistral-template.yaml index 23f37fdced..d2bee935e4 100644 --- a/gateway/gateway-controller/default-llm-provider-templates/mistral-template.yaml +++ b/gateway/gateway-controller/default-llm-provider-templates/mistral-template.yaml @@ -22,7 +22,7 @@ metadata: name: mistralai spec: displayName: MistralAI - groupId: mistralai + groupId: wso2-mistralai managedBy: wso2 version: v1.0 promptTokens: diff --git a/gateway/gateway-controller/default-llm-provider-templates/openai-template.yaml b/gateway/gateway-controller/default-llm-provider-templates/openai-template.yaml index c19ae69749..b22d6ae62f 100644 --- a/gateway/gateway-controller/default-llm-provider-templates/openai-template.yaml +++ b/gateway/gateway-controller/default-llm-provider-templates/openai-template.yaml @@ -22,7 +22,7 @@ metadata: name: openai spec: displayName: OpenAI - groupId: openai + groupId: wso2-openai managedBy: wso2 version: v1.0 promptTokens: diff --git a/platform-api/internal/apperror/catalog.go b/platform-api/internal/apperror/catalog.go index 5d28384785..313348bb84 100644 --- a/platform-api/internal/apperror/catalog.go +++ b/platform-api/internal/apperror/catalog.go @@ -102,6 +102,7 @@ var ( LLMProviderTemplateDisabled = def(CodeLLMProviderTemplateDisabled, http.StatusBadRequest, "The referenced LLM provider template is disabled.") LLMProviderTemplateReadOnly = def(CodeLLMProviderTemplateReadOnly, http.StatusForbidden, "Built-in templates are read-only and cannot be modified.") LLMProviderTemplateNotToggleable = def(CodeLLMProviderTemplateNotToggleable, http.StatusForbidden, "Only built-in templates can be enabled or disabled.") + LLMProviderTemplateBuiltInImmutable = def(CodeLLMProviderTemplateBuiltInImmutable, http.StatusForbidden, "Built-in templates cannot have new versions. Create a copy to make a custom template.") ) // Gateway entries. diff --git a/platform-api/internal/apperror/codes.go b/platform-api/internal/apperror/codes.go index fcf757fd0d..7e55f34055 100644 --- a/platform-api/internal/apperror/codes.go +++ b/platform-api/internal/apperror/codes.go @@ -68,6 +68,7 @@ const ( CodeLLMProviderTemplateDisabled = "LLM_PROVIDER_TEMPLATE_DISABLED" CodeLLMProviderTemplateReadOnly = "LLM_PROVIDER_TEMPLATE_READ_ONLY" CodeLLMProviderTemplateNotToggleable = "LLM_PROVIDER_TEMPLATE_NOT_TOGGLEABLE" + CodeLLMProviderTemplateBuiltInImmutable = "LLM_PROVIDER_TEMPLATE_BUILT_IN_IMMUTABLE" ) // Gateway domain codes, matching the examples documented in resources/openapi.yaml. diff --git a/platform-api/internal/constants/constants.go b/platform-api/internal/constants/constants.go index 390154f21c..9ad7a4f38a 100644 --- a/platform-api/internal/constants/constants.go +++ b/platform-api/internal/constants/constants.go @@ -169,6 +169,12 @@ const ( const TemplateManagedByOrganization = "organization" +// ReservedTemplateGroupIDPrefix is the group_id namespace reserved for WSO2-shipped +// built-in templates. Custom templates created via the REST API must not use it; a +// generated group_id that falls in this namespace is rewritten with the "x" prefix +// (e.g. "wso2-openai" -> "xwso2-openai"). +const ReservedTemplateGroupIDPrefix = "wso2-" + // ValidPolicyManagedBy holds accepted values for the managed_by field on gateway custom policies var ValidPolicyManagedBy = map[string]bool{ PolicyManagedByOrganization: true, diff --git a/platform-api/internal/repository/interfaces.go b/platform-api/internal/repository/interfaces.go index e09724ac67..21224e6dea 100644 --- a/platform-api/internal/repository/interfaces.go +++ b/platform-api/internal/repository/interfaces.go @@ -238,6 +238,7 @@ type LLMProviderTemplateRepository interface { Exists(templateID, orgUUID string) (bool, error) GetGroupID(handle, orgUUID string) (string, error) ManagedByForHandle(handle, orgUUID string) (string, error) + ManagedByForGroupID(groupID, orgUUID string) (string, error) CountProvidersUsingTemplate(templateID, orgUUID, version string) (int, error) } diff --git a/platform-api/internal/repository/llm.go b/platform-api/internal/repository/llm.go index 7f692408b4..cdb06a2b2b 100644 --- a/platform-api/internal/repository/llm.go +++ b/platform-api/internal/repository/llm.go @@ -258,6 +258,22 @@ func (r *LLMProviderTemplateRepo) ManagedByForHandle(handle, orgUUID string) (st return managedBy, nil } +// ManagedByForGroupID returns the managed_by value for a template family (group_id). +// Returns an empty string when the family does not exist. +func (r *LLMProviderTemplateRepo) ManagedByForGroupID(groupID, orgUUID string) (string, error) { + var managedBy string + err := r.db.QueryRow(r.db.Rebind(` + SELECT managed_by FROM llm_provider_templates WHERE group_id = ? AND organization_uuid = ? + ORDER BY (SELECT NULL) `+r.db.FetchFirstClause(1)), groupID, orgUUID).Scan(&managedBy) + if errors.Is(err, sql.ErrNoRows) { + return "", nil + } + if err != nil { + return "", err + } + return managedBy, nil +} + func (r *LLMProviderTemplateRepo) GetByID(templateID, orgUUID string) (*model.LLMProviderTemplate, error) { row := r.db.QueryRow(r.db.Rebind(` SELECT uuid, organization_uuid, handle, group_id, display_name, managed_by, description, created_by, updated_by, diff --git a/platform-api/internal/service/llm.go b/platform-api/internal/service/llm.go index f860b926b2..96089b952f 100644 --- a/platform-api/internal/service/llm.go +++ b/platform-api/internal/service/llm.go @@ -215,6 +215,7 @@ func (s *LLMProviderTemplateService) Create(orgUUID, createdBy string, req *api. if err != nil || baseHandle == "" { return nil, apperror.ValidationFailed.New("The displayName must contain at least one alphanumeric character.") } + baseHandle = applyReservedGroupIDGuard(baseHandle) version := "v1.0" if v := req.Version; v != "" { normalized, ok := normalizeTemplateVersion(v) @@ -471,6 +472,16 @@ func makeTemplateHandle(baseHandle, version string) string { return baseHandle + "-" + strings.ReplaceAll(strings.ToLower(strings.TrimSpace(version)), ".", "-") } +// applyReservedGroupIDGuard rewrites a generated custom-template group_id that falls in +// the reserved WSO2 namespace ("wso2" exactly, or a "wso2-" prefix) to the "xwso2-" +// namespace, so custom templates can never collide with WSO2 built-ins. +func applyReservedGroupIDGuard(baseHandle string) string { + if baseHandle == constants.PolicyManagedByWSO2 || strings.HasPrefix(baseHandle, constants.ReservedTemplateGroupIDPrefix) { + return "x" + baseHandle + } + return baseHandle +} + func templateVersionCreatable(v string) bool { major, _, ok := strings.Cut(strings.TrimPrefix(v, "v"), ".") if !ok { @@ -504,6 +515,18 @@ func (s *LLMProviderTemplateService) CreateVersion(orgUUID, groupID, createdBy s if count == 0 { return nil, apperror.LLMProviderTemplateNotFound.New() } + + // Built-in (WSO2-managed) families are immutable via the REST API: new versions are + // only ever added by WSO2 through the seed data. Users must instead copy a built-in, + // which creates a new custom family through Create. + familyManagedBy, err := s.repo.ManagedByForGroupID(groupID, orgUUID) + if err != nil { + return nil, fmt.Errorf("failed to resolve template family owner: %w", err) + } + if familyManagedBy == constants.PolicyManagedByWSO2 { + return nil, apperror.LLMProviderTemplateBuiltInImmutable.New() + } + baseHandle := groupID m := &model.LLMProviderTemplate{ diff --git a/platform-api/internal/service/llm_provider_template_test.go b/platform-api/internal/service/llm_provider_template_test.go index e6890010c9..3bd6ce5c7b 100644 --- a/platform-api/internal/service/llm_provider_template_test.go +++ b/platform-api/internal/service/llm_provider_template_test.go @@ -48,6 +48,9 @@ type mockLLMProviderTemplateCRUDRepo struct { getGroupIDResult string getGroupIDErr error + managedByForGroupIDResult string + managedByForGroupIDErr error + renameFamilyCalled bool renameFamilyBase string renameFamilyOrg string @@ -105,6 +108,10 @@ func (m *mockLLMProviderTemplateCRUDRepo) GetGroupID(handle, orgUUID string) (st return m.getGroupIDResult, m.getGroupIDErr } +func (m *mockLLMProviderTemplateCRUDRepo) ManagedByForGroupID(groupID, orgUUID string) (string, error) { + return m.managedByForGroupIDResult, m.managedByForGroupIDErr +} + func (m *mockLLMProviderTemplateCRUDRepo) RenameFamily(baseHandle, orgUUID, name string) error { m.renameFamilyCalled = true m.renameFamilyBase = baseHandle @@ -193,6 +200,29 @@ func TestLLMProviderTemplateServiceCreate_Success(t *testing.T) { } } +func TestLLMProviderTemplateServiceCreate_RewritesReservedWSO2Prefix(t *testing.T) { + cases := map[string]string{ + "wso2 openai": "xwso2-openai", // slugifies to "wso2-openai" + "WSO2 Template": "xwso2-template", + "wso2": "xwso2", + } + for displayName, wantGroupID := range cases { + repo := &mockLLMProviderTemplateCRUDRepo{} + svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) + + resp, err := svc.Create("org-1", "alice", validTemplateRequest(displayName)) + if err != nil { + t.Fatalf("[%s] expected no error, got: %v", displayName, err) + } + if repo.created == nil || repo.created.GroupID != wantGroupID { + t.Fatalf("[%s] expected reserved prefix rewrite to group_id %q, got: %#v", displayName, wantGroupID, repo.created) + } + if resp == nil || resp.Id == nil || !strings.HasPrefix(*resp.Id, wantGroupID) { + t.Fatalf("[%s] expected handle derived from %q, got: %#v", displayName, wantGroupID, resp) + } + } +} + func TestLLMProviderTemplateServiceCreate_RejectsMissingEndpoint(t *testing.T) { repo := &mockLLMProviderTemplateCRUDRepo{} svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) @@ -373,7 +403,7 @@ func TestLLMProviderTemplateServiceUpdate_PropagatesNameToFamily(t *testing.T) { // ---- CreateVersion ---- func TestLLMProviderTemplateServiceCreateVersion_Success(t *testing.T) { - repo := &mockLLMProviderTemplateCRUDRepo{countVersionsResult: 1} + repo := &mockLLMProviderTemplateCRUDRepo{countVersionsResult: 1, managedByForGroupIDResult: constants.TemplateManagedByOrganization} svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) req := &api.CreateLLMProviderTemplateVersionRequest{DisplayName: stringPtr("Mistral"), Version: "v2.0"} @@ -392,24 +422,17 @@ func TestLLMProviderTemplateServiceCreateVersion_Success(t *testing.T) { } } -func TestLLMProviderTemplateServiceCreateVersion_ForkFromBuiltinSetsOrganizationManagedBy(t *testing.T) { - repo := &mockLLMProviderTemplateCRUDRepo{countVersionsResult: 1} +func TestLLMProviderTemplateServiceCreateVersion_RejectsNewVersionOnBuiltinFamily(t *testing.T) { + repo := &mockLLMProviderTemplateCRUDRepo{countVersionsResult: 1, managedByForGroupIDResult: constants.PolicyManagedByWSO2} svc := NewLLMProviderTemplateService(repo, &noopAuditRepo{}, newTestIdentityService()) - wso2 := "wso2" - req := &api.CreateLLMProviderTemplateVersionRequest{DisplayName: stringPtr("Mistral"), Version: "v2.0", ManagedBy: &wso2} - resp, err := svc.CreateVersion("org-1", "mistralai", "test-user", req) - if err != nil { - t.Fatalf("expected no error when forking a built-in, got: %v", err) - } - if resp == nil { - t.Fatal("expected a response, got nil") - } - if repo.createdVersion == nil { - t.Fatal("expected CreateNewVersion to be called") + req := &api.CreateLLMProviderTemplateVersionRequest{DisplayName: stringPtr("Mistral"), Version: "v2.0"} + _, err := svc.CreateVersion("org-1", "wso2-mistralai", "test-user", req) + if !apperror.LLMProviderTemplateBuiltInImmutable.Is(err) { + t.Fatalf("expected LLMProviderTemplateBuiltInImmutable when adding a version to a built-in family, got: %v", err) } - if repo.createdVersion.ManagedBy != constants.TemplateManagedByOrganization { - t.Fatalf("expected forked version to have managedBy='organization', got: %q", repo.createdVersion.ManagedBy) + if repo.createdVersion != nil { + t.Fatalf("expected no version to be created for a built-in family, got: %#v", repo.createdVersion) } } diff --git a/platform-api/resources/default-llm-provider-templates/anthropic-template.yaml b/platform-api/resources/default-llm-provider-templates/anthropic-template.yaml index 5ae3846b43..ef4e45e65e 100644 --- a/platform-api/resources/default-llm-provider-templates/anthropic-template.yaml +++ b/platform-api/resources/default-llm-provider-templates/anthropic-template.yaml @@ -19,7 +19,7 @@ metadata: name: anthropic spec: - groupId: anthropic + groupId: wso2-anthropic displayName: Anthropic managedBy: wso2 version: v1.0 diff --git a/platform-api/resources/default-llm-provider-templates/awsbedrock-template.yaml b/platform-api/resources/default-llm-provider-templates/awsbedrock-template.yaml index e29da853b5..c052af7e4d 100644 --- a/platform-api/resources/default-llm-provider-templates/awsbedrock-template.yaml +++ b/platform-api/resources/default-llm-provider-templates/awsbedrock-template.yaml @@ -19,7 +19,7 @@ metadata: name: awsbedrock spec: - groupId: awsbedrock + groupId: wso2-awsbedrock displayName: AWS Bedrock managedBy: wso2 version: v1.0 diff --git a/platform-api/resources/default-llm-provider-templates/azureaifoundry-template.yaml b/platform-api/resources/default-llm-provider-templates/azureaifoundry-template.yaml index 02c084e406..464e2feb6b 100644 --- a/platform-api/resources/default-llm-provider-templates/azureaifoundry-template.yaml +++ b/platform-api/resources/default-llm-provider-templates/azureaifoundry-template.yaml @@ -19,7 +19,7 @@ metadata: name: azureai-foundry spec: - groupId: azureai-foundry + groupId: wso2-azureai-foundry displayName: Azure AI Foundry managedBy: wso2 version: v1.0 diff --git a/platform-api/resources/default-llm-provider-templates/azureopenai-template.yaml b/platform-api/resources/default-llm-provider-templates/azureopenai-template.yaml index 82808b0f85..7386e32a09 100644 --- a/platform-api/resources/default-llm-provider-templates/azureopenai-template.yaml +++ b/platform-api/resources/default-llm-provider-templates/azureopenai-template.yaml @@ -19,7 +19,7 @@ metadata: name: azure-openai spec: - groupId: azure-openai + groupId: wso2-azure-openai displayName: Azure OpenAI managedBy: wso2 version: v1.0 diff --git a/platform-api/resources/default-llm-provider-templates/gemini-template.yaml b/platform-api/resources/default-llm-provider-templates/gemini-template.yaml index 9aee0c5022..deb2a408eb 100644 --- a/platform-api/resources/default-llm-provider-templates/gemini-template.yaml +++ b/platform-api/resources/default-llm-provider-templates/gemini-template.yaml @@ -19,7 +19,7 @@ metadata: name: gemini spec: - groupId: gemini + groupId: wso2-gemini displayName: Gemini managedBy: wso2 version: v1.0 diff --git a/platform-api/resources/default-llm-provider-templates/mistral-template.yaml b/platform-api/resources/default-llm-provider-templates/mistral-template.yaml index 5b04efabe3..1f8536e140 100644 --- a/platform-api/resources/default-llm-provider-templates/mistral-template.yaml +++ b/platform-api/resources/default-llm-provider-templates/mistral-template.yaml @@ -19,7 +19,7 @@ metadata: name: mistralai spec: - groupId: mistralai + groupId: wso2-mistralai displayName: Mistral managedBy: wso2 version: v1.0 diff --git a/platform-api/resources/default-llm-provider-templates/openai-template.yaml b/platform-api/resources/default-llm-provider-templates/openai-template.yaml index ba0490e127..0feaae6bd7 100644 --- a/platform-api/resources/default-llm-provider-templates/openai-template.yaml +++ b/platform-api/resources/default-llm-provider-templates/openai-template.yaml @@ -19,7 +19,7 @@ metadata: name: openai spec: - groupId: openai + groupId: wso2-openai displayName: OpenAI managedBy: wso2 version: v1.0 diff --git a/platform-api/resources/openapi.yaml b/platform-api/resources/openapi.yaml index bb184bb3d3..18dd99b757 100644 --- a/platform-api/resources/openapi.yaml +++ b/platform-api/resources/openapi.yaml @@ -1055,7 +1055,7 @@ paths: - `query=groupId:&version:` -> retrieve a specific template version All filters are provided via the URL-encoded `query` parameter (e.g. - `?query=groupId%3Aopenai%26version%3Av2.0`). + `?query=groupId%3Awso2-openai%26version%3Av2.0`). Returns a single `LLMProviderTemplate` only when both `groupId` and `version` are specified; otherwise returns an `LLMProviderTemplateListResponse`. @@ -1076,10 +1076,10 @@ paths: versions; adding `&version:` returns the single full template for that version. Terms are `&`-separated `key:value` pairs and the whole value is percent-encoded (e.g. - groupId%3Aopenai%26version%3Av2.0). + groupId%3Awso2-openai%26version%3Av2.0). schema: type: string - example: groupId:openai&version:v2.0 + example: groupId:wso2-openai&version:v2.0 - $ref: '#/components/parameters/limit-Q' - $ref: '#/components/parameters/offset-Q' responses: @@ -1119,6 +1119,11 @@ paths: is rejected. `toTemplateId`, when supplied, must equal the handle derived from the family and `toVersion`. Organization is identified via the JWT token. + + Built-in (WSO2-managed) template families are immutable: adding a version + to one is rejected with `403`. To base a custom template on a built-in, + create a new template (`POST /llm-provider-templates`) instead — it gets + its own `group_id` and starts at v1.0. operationId: copyLLMProviderTemplateVersion security: - OAuth2Security: diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/CreateProviderTemplate.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/CreateProviderTemplate.tsx index 50cdccd882..64868a6ec8 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/CreateProviderTemplate.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/CreateProviderTemplate.tsx @@ -17,7 +17,7 @@ */ import React, { useRef, useState } from 'react'; -import { useNavigate, Link as RouterLink } from 'react-router-dom'; +import { useNavigate, useLocation, Link as RouterLink } from 'react-router-dom'; import YAML from 'yaml'; import { Accordion, @@ -45,13 +45,17 @@ import { buildOrgPath } from '../../../../utils/projectRouting'; import useAIWorkspaceSnackbar from '../../../../hooks/aiWorkspaceSnackbar'; import type { CreateProviderTemplateRequest, + ProviderTemplate, + ResourceMappings, TemplateMetadata, + TemplateMetadataAuth, } from '../../../../utils/types'; import { DEFAULT_AUTH_CONFIG, DEFAULT_TOKEN_CONFIG, fromTokenConfig, isValidHttpUrl, + toTokenConfig, TOKEN_FIELDS, TOKEN_LOCATIONS, type TokenConfig, @@ -99,22 +103,41 @@ function isParseableSpec(text: string): boolean { export default function CreateProviderTemplate() { const navigate = useNavigate(); + const location = useLocation(); const { currentOrganization } = useAppShell(); const { createTemplate } = useProviderTemplates(); const showSnackbar = useAIWorkspaceSnackbar(); - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); - const [endpointUrl, setEndpointUrl] = useState(''); - const [openapiSpecUrl, setOpenapiSpecUrl] = useState(''); + const copyFrom = (location.state as { copyFrom?: ProviderTemplate } | null) + ?.copyFrom; + + const initialName = copyFrom ? `${copyFrom.displayName} Copy` : ''; + const [name, setName] = useState(initialName); + const [description, setDescription] = useState(copyFrom?.description ?? ''); + const [endpointUrl, setEndpointUrl] = useState( + copyFrom?.metadata?.endpointUrl ?? '' + ); + const [openapiSpecUrl, setOpenapiSpecUrl] = useState( + copyFrom?.metadata?.openapiSpecUrl ?? '' + ); const [isFetchingSpec, setIsFetchingSpec] = useState(false); const [specFileName, setSpecFileName] = useState(''); - const [specContent, setSpecContent] = useState(''); - const [specFetched, setSpecFetched] = useState(false); + const [specContent, setSpecContent] = useState(copyFrom?.openapi ?? ''); + const [specFetched, setSpecFetched] = useState( + Boolean(copyFrom?.metadata?.openapiSpecUrl || copyFrom?.openapi) + ); - const [tokenConfig, setTokenConfig] = useState(() => ({ - ...DEFAULT_TOKEN_CONFIG, - })); + const copiedAuth = useRef( + copyFrom?.metadata?.auth + ); + const copiedLogoUrl = useRef(copyFrom?.metadata?.logoUrl); + const copiedResourceMappings = useRef( + copyFrom?.resourceMappings + ); + + const [tokenConfig, setTokenConfig] = useState(() => + copyFrom ? toTokenConfig(copyFrom) : { ...DEFAULT_TOKEN_CONFIG } + ); const [nameTouched, setNameTouched] = useState(false); const [specUrlTouched, setSpecUrlTouched] = useState(false); const [endpointUrlTouched, setEndpointUrlTouched] = useState(false); @@ -226,7 +249,10 @@ export default function CreateProviderTemplate() { if (endpointUrl.trim()) metadata.endpointUrl = endpointUrl.trim(); if (openapiSpecUrl.trim()) metadata.openapiSpecUrl = openapiSpecUrl.trim(); - metadata.auth = { ...DEFAULT_AUTH_CONFIG }; + metadata.auth = copiedAuth.current + ? { ...copiedAuth.current } + : { ...DEFAULT_AUTH_CONFIG }; + if (copiedLogoUrl.current) metadata.logoUrl = copiedLogoUrl.current; const payload: CreateProviderTemplateRequest = { id: normalizedTemplateId, @@ -235,6 +261,7 @@ export default function CreateProviderTemplate() { description: description.trim() || undefined, ...tokenFields, metadata: Object.keys(metadata).length ? metadata : undefined, + resourceMappings: copiedResourceMappings.current, openapi: specContent.trim() ? specContent : undefined, }; @@ -268,12 +295,30 @@ export default function CreateProviderTemplate() { - + {copyFrom ? ( + + ) : ( + + )} + {copyFrom && ( + + + + )} {/* component="form" makes Enter submit and groups inputs semantically. */} diff --git a/portals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/ProviderTemplateOverview.tsx b/portals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/ProviderTemplateOverview.tsx index ffb87dd222..71fb1c36cc 100644 --- a/portals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/ProviderTemplateOverview.tsx +++ b/portals/ai-workspace/src/pages/appShell/appShellPages/providerTemplate/ProviderTemplateOverview.tsx @@ -45,7 +45,7 @@ import { Tooltip, Typography, } from '@wso2/oxygen-ui'; -import { Check, ChevronDown, ChevronLeft, Clock, Download, Edit, GitBranch, Lock, Trash2 } from '@wso2/oxygen-ui-icons-react'; +import { Check, ChevronDown, ChevronLeft, Clock, Copy, Download, Edit, GitBranch, Lock, Trash2 } from '@wso2/oxygen-ui-icons-react'; import { FormattedMessage } from 'react-intl'; import { formatRelativeTime } from '../../../../contexts/llmProvider'; import { useProviderTemplates } from '../../../../contexts/llmProvider/providerTemplate'; @@ -779,16 +779,20 @@ export default function ProviderTemplateOverview() { ); })} - - setVersionMenuAnchor(null)} - sx={{ color: 'primary.main', gap: 1 }} - > - - Create new version - + {!isBuiltIn && ( + <> + + setVersionMenuAnchor(null)} + sx={{ color: 'primary.main', gap: 1 }} + > + + Create new version + + + )} ); })()} @@ -826,6 +830,18 @@ export default function ProviderTemplateOverview() { + {isBuiltIn && ( + + )} {!isBuiltIn && (