Skip to content
Draft
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
9 changes: 8 additions & 1 deletion platform-api/internal/handler/llm.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,15 @@ func (h *LLMHandler) ListLLMProviders(w http.ResponseWriter, r *http.Request) er
}

limit, offset := parsePagination(r)
customPolicyUUID := strings.TrimSpace(r.URL.Query().Get("customPolicyUuid"))

resp, err := h.providerService.List(orgID, limit, offset)
var resp *api.LLMProviderListResponse
var err error
if customPolicyUUID != "" {
resp, err = h.providerService.ListByCustomPolicy(orgID, customPolicyUUID, limit, offset)
} else {
resp, err = h.providerService.List(orgID, limit, offset)
}
if err != nil {
return serviceError(err, fmt.Sprintf("failed to list LLM providers in org %s", orgID))
}
Expand Down
2 changes: 2 additions & 0 deletions platform-api/internal/repository/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ type LLMProviderRepository interface {
GetByID(providerID, orgUUID string) (*model.LLMProvider, error)
List(orgUUID string, limit, offset int) ([]*model.LLMProvider, error)
Count(orgUUID string) (int, error)
ListByCustomPolicy(orgUUID, policyUUID string, limit, offset int) ([]*model.LLMProvider, error)
CountByCustomPolicy(orgUUID, policyUUID string) (int, error)
Update(p *model.LLMProvider) error
UpdateWithCustomPolicyUsages(p *model.LLMProvider, policyUUIDs []string) error
Delete(providerID, orgUUID string) error
Expand Down
65 changes: 65 additions & 0 deletions platform-api/internal/repository/llm.go
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,71 @@ func (r *LLMProviderRepo) Count(orgUUID string) (int, error) {
return r.artifactRepo.CountByKindAndOrg(constants.LLMProvider, orgUUID)
}

func (r *LLMProviderRepo) ListByCustomPolicy(orgUUID, policyUUID string, limit, offset int) ([]*model.LLMProvider, error) {
pageClause, pageArgs := r.db.PaginationClause(limit, offset)
args := append([]any{orgUUID, policyUUID}, pageArgs...)
query := `
SELECT
p.uuid, p.handle, p.display_name, p.version, p.organization_uuid, p.origin, p.data_version, p.created_at, p.updated_at,
p.description, p.created_by, p.updated_by, p.template_uuid, p.openapi_spec, p.model_list, p.configuration
FROM llm_providers p
INNER JOIN gateway_custom_policy_usages u ON u.artifact_uuid = p.uuid
WHERE p.organization_uuid = ? AND u.policy_uuid = ?
ORDER BY p.created_at DESC
` + pageClause
rows, err := r.db.Query(r.db.Rebind(query), args...)
if err != nil {
return nil, err
}
defer rows.Close()

var res []*model.LLMProvider
for rows.Next() {
var p model.LLMProvider
var createdBy, updatedBy sql.NullString
var openAPISpec, modelProvidersRaw []byte
var configurationJSON []byte
err := rows.Scan(
&p.UUID, &p.ID, &p.Name, &p.Version, &p.OrganizationUUID, &p.Origin, &p.DataVersion, &p.CreatedAt, &p.UpdatedAt,
&p.Description, &createdBy, &updatedBy, &p.TemplateUUID, &openAPISpec, &modelProvidersRaw, &configurationJSON,
)
if err != nil {
return nil, err
}
p.CreatedBy = createdBy.String
p.UpdatedBy = updatedBy.String
if len(openAPISpec) > 0 {
p.OpenAPISpec = string(openAPISpec)
}
if len(modelProvidersRaw) > 0 {
if err := json.Unmarshal(modelProvidersRaw, &p.ModelProviders); err != nil {
return nil, fmt.Errorf("unmarshal modelProviders for provider %s: %w", p.ID, err)
}
}
if len(configurationJSON) > 0 {
if config, err := deserializeLLMProviderConfiguration(configurationJSON); err != nil {
return nil, fmt.Errorf("unmarshal configuration for provider %s: %w", p.ID, err)
} else if config != nil {
p.Configuration = *config
}
}
res = append(res, &p)
}
return res, rows.Err()
}

func (r *LLMProviderRepo) CountByCustomPolicy(orgUUID, policyUUID string) (int, error) {
var count int
query := `
SELECT COUNT(*) FROM llm_providers p
INNER JOIN gateway_custom_policy_usages u ON u.artifact_uuid = p.uuid
WHERE p.organization_uuid = ? AND u.policy_uuid = ?`
if err := r.db.QueryRow(r.db.Rebind(query), orgUUID, policyUUID).Scan(&count); err != nil {
return 0, err
}
return count, nil
}

func (r *LLMProviderRepo) Update(p *model.LLMProvider) error {
return r.update(p, nil, false)
}
Expand Down
71 changes: 71 additions & 0 deletions platform-api/internal/service/llm.go
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,77 @@ func (s *LLMProviderService) List(orgUUID string, limit, offset int) (*api.LLMPr
return resp, nil
}

// ListByCustomPolicy lists LLM providers that reference the given custom policy UUID.
func (s *LLMProviderService) ListByCustomPolicy(orgUUID, customPolicyUUID string, limit, offset int) (*api.LLMProviderListResponse, error) {
if customPolicyUUID == "" {
return nil, apperror.ValidationFailed.New("The custom policy uuid is required.")
}
if s.customPolicyRepo == nil {
return nil, fmt.Errorf("could not initialize custom policy repository")
}
policy, err := s.customPolicyRepo.GetCustomPolicyByUUID(orgUUID, customPolicyUUID)
if err != nil {
return nil, fmt.Errorf("failed to validate custom policy: %w", err)
}
if policy == nil {
return nil, apperror.CustomPolicyNotFound.New()
}

items, err := s.repo.ListByCustomPolicy(orgUUID, customPolicyUUID, limit, offset)
if err != nil {
return nil, fmt.Errorf("failed to list providers by custom policy: %w", err)
}
totalCount, err := s.repo.CountByCustomPolicy(orgUUID, customPolicyUUID)
if err != nil {
return nil, fmt.Errorf("failed to count providers by custom policy: %w", err)
}
resp := &api.LLMProviderListResponse{
Count: len(items),
Pagination: api.Pagination{
Limit: limit,
Offset: offset,
Total: totalCount,
},
}
resp.List = make([]api.LLMProviderListItem, 0, len(items))
createdByFields := make([]**string, 0, len(items))
for _, p := range items {
// Look up template handle from UUID
tplHandle := ""
if p.TemplateUUID != "" {
tpl, err := s.templateRepo.GetByUUID(p.TemplateUUID, orgUUID)
if err != nil {
return nil, fmt.Errorf("failed to resolve template for provider %s: %w", p.ID, err)
}
if tpl != nil {
tplHandle = tpl.ID
}
}
id := p.ID
name := p.Name
desc := utils.StringPtrIfNotEmpty(p.Description)
createdBy := utils.StringPtrIfNotEmpty(p.CreatedBy)
version := p.Version
template := utils.StringPtrIfNotEmpty(tplHandle)
resp.List = append(resp.List, api.LLMProviderListItem{
Id: &id,
DisplayName: name,
Description: desc,
CreatedBy: createdBy,
Version: &version,
Template: template,
ReadOnly: utils.BoolPtr(p.Origin == constants.OriginDP),
CreatedAt: utils.TimePtr(p.CreatedAt),
UpdatedAt: utils.TimePtr(p.UpdatedAt),
})
createdByFields = append(createdByFields, &resp.List[len(resp.List)-1].CreatedBy)
}
if err := s.identity.ResolveIdentityFields(createdByFields); err != nil {
return nil, err
}
return resp, nil
}

func (s *LLMProviderService) Get(orgUUID, handle string) (*api.LLMProvider, error) {
if handle == "" {
return nil, apperror.ValidationFailed.New("The LLM provider id is required.")
Expand Down
12 changes: 10 additions & 2 deletions platform-api/internal/service/llm_custom_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ import (

type llmCustomPolicyRepo struct {
repository.CustomPolicyRepository
policies map[string][]*model.CustomPolicy
lookupErr error
policies map[string][]*model.CustomPolicy
policyByID map[string]*model.CustomPolicy
lookupErr error
}

func (r *llmCustomPolicyRepo) GetCustomPoliciesByName(_ string, name string) ([]*model.CustomPolicy, error) {
Expand All @@ -41,6 +42,13 @@ func (r *llmCustomPolicyRepo) GetCustomPoliciesByName(_ string, name string) ([]
return r.policies[name], nil
}

func (r *llmCustomPolicyRepo) GetCustomPolicyByUUID(_ string, policyUUID string) (*model.CustomPolicy, error) {
if r.lookupErr != nil {
return nil, r.lookupErr
}
return r.policyByID[policyUUID], nil
}

func TestLLMProviderResolveCustomPolicyUUIDs(t *testing.T) {
repo := &llmCustomPolicyRepo{
policies: map[string][]*model.CustomPolicy{
Expand Down
60 changes: 54 additions & 6 deletions platform-api/internal/service/llm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1009,12 +1009,15 @@ func findOperationPath(policy *api.OperationPolicy, path string) *api.OperationP

type mockLLMProviderRepo struct {
repository.LLMProviderRepository
existsResult bool
countResult int
getByIDFunc func(providerID, orgUUID string) (*model.LLMProvider, error)
createCalled bool
created *model.LLMProvider
updated *model.LLMProvider
existsResult bool
countResult int
getByIDFunc func(providerID, orgUUID string) (*model.LLMProvider, error)
createCalled bool
created *model.LLMProvider
updated *model.LLMProvider
listByCustomPolicyItems []*model.LLMProvider
countByCustomPolicyValue int
lastListCustomPolicyUUID string
}

func (m *mockLLMProviderRepo) Exists(providerID, orgUUID string) (bool, error) {
Expand Down Expand Up @@ -1051,6 +1054,15 @@ func (m *mockLLMProviderRepo) UpdateWithCustomPolicyUsages(p *model.LLMProvider,
return m.Update(p)
}

func (m *mockLLMProviderRepo) ListByCustomPolicy(orgUUID, policyUUID string, limit, offset int) ([]*model.LLMProvider, error) {
m.lastListCustomPolicyUUID = policyUUID
return m.listByCustomPolicyItems, nil
}

func (m *mockLLMProviderRepo) CountByCustomPolicy(orgUUID, policyUUID string) (int, error) {
return m.countByCustomPolicyValue, nil
}

type mockLLMTemplateRepo struct {
repository.LLMProviderTemplateRepository
getByIDFunc func(templateID, orgUUID string) (*model.LLMProviderTemplate, error)
Expand Down Expand Up @@ -1526,6 +1538,42 @@ func TestLLMProxyServiceListByProviderUsesProviderUUID(t *testing.T) {
}
}

func TestLLMProviderServiceListByCustomPolicyUsesPolicyUUID(t *testing.T) {
now := time.Now()
providerRepo := &mockLLMProviderRepo{
listByCustomPolicyItems: []*model.LLMProvider{{
UUID: "provider-uuid",
ID: "provider-1",
Name: "Provider One",
Version: "v1.0",
CreatedAt: now,
UpdatedAt: now,
}},
countByCustomPolicyValue: 1,
}
customPolicyRepo := &llmCustomPolicyRepo{
policyByID: map[string]*model.CustomPolicy{
"policy-uuid": {UUID: "policy-uuid", Name: "rate-limit", Version: "v1.0.0"},
},
}
service := NewLLMProviderService(providerRepo, nil, nil, nil, nil, nil, nil, slog.Default(), &noopAuditRepo{}, &config.Server{}, newTestIdentityService())
service.SetCustomPolicyRepository(customPolicyRepo)

resp, err := service.ListByCustomPolicy("org-1", "policy-uuid", 10, 0)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
if providerRepo.lastListCustomPolicyUUID != "policy-uuid" {
t.Fatalf("expected list by custom policy to use policy UUID, got: %q", providerRepo.lastListCustomPolicyUUID)
}
if resp == nil || resp.Count != 1 || len(resp.List) != 1 {
t.Fatalf("expected one provider in response, got: %#v", resp)
}
if resp.List[0].DisplayName != "Provider One" {
t.Fatalf("expected provider display name to round-trip, got: %q", resp.List[0].DisplayName)
}
}

func TestLLMProxyServiceUpdatePreservesProviderAuthValue(t *testing.T) {
now := time.Now()
proxyRepo := &mockLLMProxyRepo{}
Expand Down
12 changes: 12 additions & 0 deletions platform-api/resources/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,7 @@ paths:
tags:
- LLM Providers
parameters:
- $ref: '#/components/parameters/customPolicyUuid-Q'
- $ref: '#/components/parameters/limit-Q'
- $ref: '#/components/parameters/offset-Q'
responses:
Expand Down Expand Up @@ -8871,6 +8872,17 @@ components:
type: string
example: "default-project"

customPolicyUuid-Q:
name: customPolicyUuid
in: query
required: false
description: |
Filter the returned LLM providers to only those referencing the custom policy with this UUID.
schema:
type: string
format: uuid
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

gatewayId-Q:
name: gatewayId
in: query
Expand Down
11 changes: 7 additions & 4 deletions portals/ai-workspace/src/apis/llmProviderApis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,25 +187,28 @@ export async function createLLMProvider(
}

/**
* Get all LLM Providers
* Get all LLM Providers, optionally filtered to those referencing a custom policy
*
* @param organizationId - The organization ID
* @param baseUrl - The platform API base URL
* @param customPolicyUuid - When provided, only providers using this custom policy are returned
* @returns Promise with the list of LLM providers
*
* @example
* ```ts
* const response = await getLLMProviders('org-uuid');
* const response = await getLLMProviders('org-uuid', baseUrl);
* console.log(response); // { count: 1, list: [...], pagination: {...} }
* ```
*/
export async function getLLMProviders(
organizationId: string,
baseUrl: string
baseUrl: string,
customPolicyUuid?: string
): Promise<LLMProvidersResponse> {
try {
const response = await get<LLMProvidersResponse>(
`/llm-providers`,
undefined,
customPolicyUuid ? { customPolicyUuid } : undefined,
baseUrl
);
return response;
Expand Down
Loading
Loading