Files
2026-07-13 13:33:09 +08:00

306 lines
8.8 KiB
Go

package shared
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
type SchemaUrl string
const (
SchemaUrlInputConfig SchemaUrl = "https://plandex.ai/schemas/models-input.schema.json"
SchemaUrlPlanConfig SchemaUrl = "https://plandex.ai/schemas/plan-config.schema.json"
SchemaUrlInlineModelPack SchemaUrl = "https://plandex.ai/schemas/model-pack-inline.schema.json"
)
// Note that none of the custom model structs should have maps anywhere in the hierarchy, since it will break deterministic hashing. Use structs or slices instead.
type CustomModel struct {
Id string `json:"id,omitempty"`
ModelId ModelId `json:"modelId"`
Publisher ModelPublisher `json:"publisher"`
Description string `json:"description"`
BaseModelShared
Providers []BaseModelUsesProvider `json:"providers"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
type CustomProvider struct {
Id string `json:"id,omitempty"`
Name string `json:"name"`
BaseUrl string `json:"baseUrl"`
// for AWS Bedrock models
HasAWSAuth bool `json:"hasAWSAuth,omitempty"`
// for local models that don't require auth (ollama, etc.)
SkipAuth bool `json:"skipAuth,omitempty"`
ApiKeyEnvVar string `json:"apiKeyEnvVar,omitempty"`
ExtraAuthVars []ModelProviderExtraAuthVars `json:"extraAuthVars,omitempty"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
type ModelsInput struct {
CustomModels []*CustomModel `json:"models,omitempty"`
CustomProviders []*CustomProvider `json:"providers,omitempty"`
CustomModelPacks []*ModelPackSchema `json:"modelPacks,omitempty"`
}
func (input ModelsInput) FilterUnchanged(existing *ModelsInput) ModelsInput {
filtered := ModelsInput{}
existingProvidersById := map[string]*CustomProvider{}
for _, provider := range existing.CustomProviders {
existingProvidersById[provider.Name] = provider
}
existingModelsById := map[string]*CustomModel{}
for _, model := range existing.CustomModels {
existingModelsById[string(model.ModelId)] = model
}
existingPacksById := map[string]*ModelPackSchema{}
for _, pack := range existing.CustomModelPacks {
existingPacksById[pack.Name] = pack
}
for _, model := range input.CustomModels {
if existingModel, ok := existingModelsById[string(model.ModelId)]; !ok || !modelsEqual(model, existingModel) {
filtered.CustomModels = append(filtered.CustomModels, model)
}
}
for _, provider := range input.CustomProviders {
if existingProvider, ok := existingProvidersById[provider.Name]; !ok || !providersEqual(provider, existingProvider) {
filtered.CustomProviders = append(filtered.CustomProviders, provider)
}
}
for _, pack := range input.CustomModelPacks {
if existingPack, ok := existingPacksById[pack.Name]; !ok || !packsEqual(pack, existingPack) {
filtered.CustomModelPacks = append(filtered.CustomModelPacks, pack)
}
}
return filtered
}
func (input ModelsInput) Equals(other ModelsInput) bool {
left := input.FilterUnchanged(&other)
right := other.FilterUnchanged(&input)
return left.IsEmpty() && right.IsEmpty()
}
func (input ModelsInput) CheckNoDuplicates() (bool, string) {
sawModelIds := map[ModelId]bool{}
sawProviderNames := map[string]bool{}
sawPackNames := map[string]bool{}
builder := strings.Builder{}
for _, provider := range input.CustomProviders {
if _, ok := sawProviderNames[provider.Name]; ok {
builder.WriteString(fmt.Sprintf("• Provider %s is duplicated\n", provider.Name))
}
sawProviderNames[provider.Name] = true
}
for _, model := range input.CustomModels {
if _, ok := sawModelIds[model.ModelId]; ok {
builder.WriteString(fmt.Sprintf("• Model %s is duplicated\n", model.ModelId))
}
sawModelIds[model.ModelId] = true
}
for _, pack := range input.CustomModelPacks {
if _, ok := sawPackNames[pack.Name]; ok {
builder.WriteString(fmt.Sprintf("• Model pack %s is duplicated\n", pack.Name))
}
sawPackNames[pack.Name] = true
}
res := builder.String()
return len(res) == 0, res
}
func (input ModelsInput) IsEmpty() bool {
return len(input.CustomModels) == 0 && len(input.CustomProviders) == 0 && len(input.CustomModelPacks) == 0
}
func modelsEqual(a, b *CustomModel) bool {
return cmp.Equal(
a, b,
cmpopts.EquateEmpty(), // treat nil == empty slice/map
cmpopts.IgnoreFields(CustomModel{}, "CreatedAt", "UpdatedAt", "Id"),
)
}
func providersEqual(a, b *CustomProvider) bool {
return cmp.Equal(
a,
b,
cmpopts.EquateEmpty(),
cmpopts.IgnoreFields(CustomProvider{}, "CreatedAt", "UpdatedAt", "Id"),
)
}
func packsEqual(a, b *ModelPackSchema) bool {
res := cmp.Equal(
a,
b,
cmpopts.EquateEmpty(),
)
return res
}
func (s *ModelPackSchema) Equals(other *ModelPackSchema) bool {
return packsEqual(s, other)
}
func (mp *ModelPack) Equals(other *ModelPack) bool {
return mp.ToModelPackSchema().Equals(other.ToModelPackSchema())
}
// Hash returns a deterministic hash of the ModelsInput.
// WARNING: This relies on json.Marshal being deterministic for our struct types.
// Do not add map fields to these structs or the hash will become non-deterministic.
func (input ModelsInput) Hash() (string, error) {
data, err := json.Marshal(input)
if err != nil {
return "", err
}
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:]), nil
}
type ClientModelPackSchema struct {
Name string `json:"name"`
Description string `json:"description"`
ClientModelPackSchemaRoles
}
func (input *ClientModelPackSchema) ToModelPackSchema() *ModelPackSchema {
return &ModelPackSchema{
Name: input.Name,
Description: input.Description,
ModelPackSchemaRoles: input.ClientModelPackSchemaRoles.ToModelPackSchemaRoles(),
}
}
func (input *ModelPackSchema) ToClientModelPackSchema() *ClientModelPackSchema {
return &ClientModelPackSchema{
Name: input.Name,
Description: input.Description,
ClientModelPackSchemaRoles: input.ToClientModelPackSchemaRoles(),
}
}
type ClientModelsInput struct {
SchemaUrl SchemaUrl `json:"$schema"`
CustomModels []*CustomModel `json:"models,omitempty"`
CustomProviders []*CustomProvider `json:"providers,omitempty"`
CustomModelPacks []*ClientModelPackSchema `json:"modelPacks,omitempty"`
}
func (input ClientModelsInput) ToModelsInput() ModelsInput {
modelPacks := []*ModelPackSchema{}
for _, pack := range input.CustomModelPacks {
modelPacks = append(modelPacks, pack.ToModelPackSchema())
}
return ModelsInput{
CustomModels: input.CustomModels,
CustomProviders: input.CustomProviders,
CustomModelPacks: modelPacks,
}
}
func (input *ClientModelsInput) PrepareUpdate() {
for _, model := range input.CustomModels {
model.Id = ""
model.CreatedAt = nil
model.UpdatedAt = nil
}
for _, provider := range input.CustomProviders {
provider.Id = ""
provider.CreatedAt = nil
provider.UpdatedAt = nil
}
}
func (input ModelsInput) ToClientModelsInput() ClientModelsInput {
clientModelPacks := []*ClientModelPackSchema{}
for _, pack := range input.CustomModelPacks {
clientModelPacks = append(clientModelPacks, pack.ToClientModelPackSchema())
}
return ClientModelsInput{
SchemaUrl: SchemaUrlInputConfig,
CustomModels: input.CustomModels,
CustomProviders: input.CustomProviders,
CustomModelPacks: clientModelPacks,
}
}
func (cp *CustomProvider) ToModelProviderConfigSchema() ModelProviderConfigSchema {
return ModelProviderConfigSchema{
Provider: ModelProviderCustom,
CustomProvider: &cp.Name,
BaseUrl: cp.BaseUrl,
HasAWSAuth: cp.HasAWSAuth,
SkipAuth: cp.SkipAuth,
ApiKeyEnvVar: cp.ApiKeyEnvVar,
ExtraAuthVars: cp.ExtraAuthVars,
}
}
func (input *CustomModel) ToBaseModelConfig(authVars map[string]string, settings *PlanSettings, orgUserConfig *OrgUserConfig) *BaseModelConfig {
providers := GetProvidersForAuthVarsWithModelId(authVars, settings, input.ModelId, orgUserConfig)
if len(providers) == 0 {
return nil
}
providerSchema := providers[0]
return input.ToBaseModelConfigForProvider(authVars, settings, &providerSchema)
}
func (input *CustomModel) ToBaseModelConfigForProvider(authVars map[string]string, settings *PlanSettings, providerSchema *ModelProviderConfigSchema) *BaseModelConfig {
var modelName ModelName
for _, usesProvider := range input.Providers {
if usesProvider.Provider == providerSchema.Provider {
modelName = usesProvider.ModelName
break
}
}
return &BaseModelConfig{
ModelTag: ModelTag(input.ModelId),
ModelId: input.ModelId,
Publisher: input.Publisher,
BaseModelShared: input.BaseModelShared,
BaseModelProviderConfig: BaseModelProviderConfig{
ModelProviderConfigSchema: *providerSchema,
ModelName: modelName,
},
}
}