@@ -13,6 +13,9 @@ import (
1313 "time"
1414)
1515
16+ // Default catalog construction, legacy conversion, and sanitization helpers
17+ // live in v1_defaults.go.
18+
1619const (
1720 CatalogV1SchemaVersion = "model-catalog/v1"
1821 // DefaultCatalogV1URL is the published model-catalog/v1 document.
@@ -635,381 +638,3 @@ func SplitOfferingIDV1(id string) (deploymentID, nativeModelID string, ok bool)
635638 left , right , found := strings .Cut (id , ":" )
636639 return left , right , found && left != "" && right != ""
637640}
638-
639- func defaultProvidersV1 () map [string ]ProviderV1 {
640- return map [string ]ProviderV1 {
641- "anthropic" : {ID : "anthropic" , Name : "Anthropic" },
642- "openai" : {ID : "openai" , Name : "OpenAI" },
643- "google" : {ID : "google" , Name : "Google" },
644- "xai" : {ID : "xai" , Name : "xAI" },
645- "openrouter" : {ID : "openrouter" , Name : "OpenRouter" },
646- "canopywave" : {ID : "canopywave" , Name : "CanopyWave" },
647- "zai_payg" : {ID : "zai_payg" , Name : "Z.AI Pay-as-you-go" },
648- "zai_coding" : {ID : "zai_coding" , Name : "Z.AI Coding Plan" },
649- "ollama" : {ID : "ollama" , Name : "Ollama" },
650- "opencodego" : {ID : "opencodego" , Name : "OpenCode Go" },
651- "moonshotai" : {ID : "moonshotai" , Name : "Moonshot AI" },
652- "kimi" : {ID : "kimi" , Name : "Kimi (Moonshot)" },
653- "xiaomi_mimo_payg" : {ID : "xiaomi_mimo_payg" , Name : "Xiaomi MiMo (Pay-as-you-go)" },
654- "xiaomi_mimo_token_plan" : {ID : "xiaomi_mimo_token_plan" , Name : "Xiaomi MiMo (Token Plan)" },
655- "deepseek" : {ID : "deepseek" , Name : "DeepSeek" },
656- }
657- }
658-
659- func defaultAPIProtocolsV1 () map [string ]APIProtocolV1 {
660- return map [string ]APIProtocolV1 {
661- "anthropic-messages" : {ID : "anthropic-messages" , Name : "Anthropic Messages" },
662- "openai-chat-completions" : {ID : "openai-chat-completions" , Name : "OpenAI Chat Completions" },
663- "gemini-generate-content" : {ID : "gemini-generate-content" , Name : "Gemini generateContent" },
664- }
665- }
666-
667- func defaultDeploymentsV1 () map [string ]DeploymentV1 {
668- return map [string ]DeploymentV1 {
669- "anthropic-direct" : deployment ("anthropic-direct" , "Anthropic" , "anthropic" , "anthropic-messages" , "anthropic" , NativeModelIDCatalogKnown ),
670- "anthropic-bedrock" : deployment ("anthropic-bedrock" , "Anthropic on Bedrock" , "anthropic" , "anthropic-messages" , "anthropic-bedrock" , NativeModelIDCatalogKnown ),
671- "anthropic-vertex" : deployment ("anthropic-vertex" , "Anthropic on Vertex" , "anthropic" , "anthropic-messages" , "anthropic-vertex" , NativeModelIDCatalogKnown ),
672- "openai-direct" : deployment ("openai-direct" , "OpenAI" , "openai" , "openai-chat-completions" , "openai" , NativeModelIDCatalogKnown ),
673- "openai-azure" : azureDeployment (),
674- "gemini-direct" : deployment ("gemini-direct" , "Gemini" , "google" , "gemini-generate-content" , "gemini" , NativeModelIDCatalogKnown ),
675- "gemini-vertex" : deployment ("gemini-vertex" , "Gemini on Vertex" , "google" , "gemini-generate-content" , "gemini-vertex" , NativeModelIDCatalogKnown ),
676- "grok-direct" : deployment ("grok-direct" , "Grok" , "xai" , "openai-chat-completions" , "grok" , NativeModelIDCatalogKnown ),
677- "openrouter" : deployment ("openrouter" , "OpenRouter" , "openrouter" , "openai-chat-completions" , "openrouter" , NativeModelIDDiscovered ),
678- "zai_payg-direct" : deployment ("zai_payg-direct" , "Z.AI Pay-as-you-go" , "zai_payg" , "openai-chat-completions" , "zai_payg" , NativeModelIDCatalogKnown ),
679- "zai_coding-direct" : deployment ("zai_coding-direct" , "Z.AI Coding Plan" , "zai_coding" , "openai-chat-completions" , "zai_coding" , NativeModelIDCatalogKnown ),
680- "canopywave" : deployment ("canopywave" , "CanopyWave" , "canopywave" , "openai-chat-completions" , "canopywave" , NativeModelIDDiscovered ),
681- "ollama-local" : localDeployment (),
682- "opencodego" : deployment ("opencodego" , "OpenCode Go" , "opencodego" , "openai-chat-completions" , "opencodego" , NativeModelIDDiscovered ),
683- "kimi-direct" : deployment ("kimi-direct" , "Kimi (Moonshot)" , "kimi" , "openai-chat-completions" , "kimi" , NativeModelIDDiscovered ),
684- "xiaomi_mimo_payg-direct" : deployment ("xiaomi_mimo_payg-direct" , "Xiaomi MiMo Pay-as-you-go" , "xiaomi_mimo_payg" , "openai-chat-completions" , "xiaomi_mimo" , NativeModelIDDiscovered ),
685- "xiaomi_mimo_token_plan-direct" : deployment ("xiaomi_mimo_token_plan-direct" , "Xiaomi MiMo Token Plan" , "xiaomi_mimo_token_plan" , "openai-chat-completions" , "xiaomi_mimo" , NativeModelIDDiscovered ),
686- "deepseek-direct" : deployment ("deepseek-direct" , "DeepSeek" , "deepseek" , "openai-chat-completions" , "deepseek" , NativeModelIDCatalogKnown ),
687- }
688- }
689-
690- func deployment (id , name , providerID , protocolID , adapter string , source NativeModelIDSource ) DeploymentV1 {
691- return DeploymentV1 {ID : id , Name : name , ProviderID : providerID , APIProtocolID : protocolID , AdapterConstructor : adapter , NativeModelIDSource : source }
692- }
693-
694- func azureDeployment () DeploymentV1 {
695- d := deployment ("openai-azure" , "Azure OpenAI" , "openai" , "openai-chat-completions" , "openai-azure" , NativeModelIDUserConfigured )
696- d .ModelMappingsRequired = true
697- return d
698- }
699-
700- func localDeployment () DeploymentV1 {
701- d := deployment ("ollama-local" , "Ollama local" , "ollama" , "openai-chat-completions" , "ollama" , NativeModelIDDiscovered )
702- d .Local = true
703- return d
704- }
705-
706- func defaultOfferingTemplatesV1 (generatedAt time.Time ) []ModelOfferingTemplateV1 {
707- var out []ModelOfferingTemplateV1
708- for _ , model := range testOpenAIModels {
709- canonical := canonicalModelID ("openai" , model .ID )
710- out = append (out , ModelOfferingTemplateV1 {
711- ID : "openai-azure:" + canonical ,
712- CanonicalModelID : canonical ,
713- DeploymentID : "openai-azure" ,
714- NativeModelIDSource : NativeModelIDUserConfigured ,
715- MappingRequired : true ,
716- Capabilities : capabilitySetFromLegacy (model ),
717- Pricing : pricingFromLegacy (model , generatedAt , "embedded" ),
718- })
719- }
720- return out
721- }
722-
723- func appendDerivedDeploymentOfferings (offerings []ModelOfferingV1 ) []ModelOfferingV1 {
724- seen := make (map [string ]bool , len (offerings ))
725- for _ , offering := range offerings {
726- seen [offering .ID ] = true
727- }
728- addCopy := func (source ModelOfferingV1 , deploymentID string ) {
729- copied := source
730- copied .DeploymentID = deploymentID
731- copied .ID = deploymentID + ":" + source .NativeModelID
732- if ! seen [copied .ID ] {
733- seen [copied .ID ] = true
734- offerings = append (offerings , copied )
735- }
736- }
737- for _ , offering := range append ([]ModelOfferingV1 (nil ), offerings ... ) {
738- switch offering .DeploymentID {
739- case "anthropic-direct" :
740- addCopy (offering , "anthropic-bedrock" )
741- addCopy (offering , "anthropic-vertex" )
742- case "gemini-direct" :
743- addCopy (offering , "gemini-vertex" )
744- }
745- }
746- return offerings
747- }
748-
749- func legacyDeploymentAndOwner (provider string ) (deploymentID , ownerProviderID string ) {
750- switch provider {
751- case "anthropic" :
752- return "anthropic-direct" , "anthropic"
753- case "openai" :
754- return "openai-direct" , "openai"
755- case "azure" :
756- return "openai-azure" , "openai"
757- case "grok" :
758- return "grok-direct" , "xai"
759- case "gemini" :
760- return "gemini-direct" , "google"
761- case "bedrock" :
762- return "anthropic-bedrock" , "anthropic"
763- case "vertex" :
764- return "gemini-vertex" , "google"
765- case "openrouter" :
766- return "openrouter" , "openrouter"
767- case "zai_payg" :
768- return "zai_payg-direct" , "zai_payg"
769- case "zai_coding" :
770- return "zai_coding-direct" , "zai_coding"
771- case "canopywave" :
772- return "canopywave" , "canopywave"
773- case "ollama" :
774- return "ollama-local" , "ollama"
775- case "opencodego" :
776- return "opencodego" , "opencodego"
777- case "kimi" , "moonshotai" :
778- return "kimi-direct" , "kimi"
779- case "xiaomi_mimo" , "xiaomi_mimo_payg" :
780- return "xiaomi_mimo_payg-direct" , "xiaomi_mimo_payg"
781- case "xiaomi_mimo_token_plan" :
782- return "xiaomi_mimo_token_plan-direct" , "xiaomi_mimo_token_plan"
783- case "deepseek" :
784- return "deepseek-direct" , "deepseek"
785- default :
786- return "" , ""
787- }
788- }
789-
790- func canonicalModelID (ownerProviderID , nativeID string ) string {
791- if strings .Contains (nativeID , "/" ) {
792- owner , _ , _ := strings .Cut (nativeID , "/" )
793- if owner != "" && ownerProviderID == canonicalProviderID (owner ) {
794- return nativeID
795- }
796- }
797- if ownerProviderID == "zai_payg" && strings .HasPrefix (nativeID , "zai/" ) {
798- return "zai_payg/" + strings .TrimPrefix (nativeID , "zai/" )
799- }
800- return ownerProviderID + "/" + nativeID
801- }
802-
803- // CanonicalProviderID normalizes legacy provider aliases (e.g. gemini -> google).
804- func CanonicalProviderID (providerID string ) string {
805- return canonicalProviderID (providerID )
806- }
807-
808- func canonicalProviderID (providerID string ) string {
809- switch providerID {
810- case "gemini" :
811- return "google"
812- case "grok" :
813- return "xai"
814- // No legacy aliases — zai_payg and zai_coding are the only valid IDs.
815- case "moonshotai" :
816- return "moonshotai"
817- case "xiaomi-mimo" , "xiaomi_mimo" , "xiaomi-mimo-payg" :
818- return "xiaomi_mimo_payg"
819- case "xiaomi-mimo-token-plan" :
820- return "xiaomi_mimo_token_plan"
821- default :
822- return providerID
823- }
824- }
825-
826- func capabilitySetFromLegacy (entry ModelCatalogEntry ) CapabilitySetV1 {
827- set := CapabilitySetV1 {
828- ServerTools : map [string ]CapabilityState {},
829- MaxInputTokens : entry .ContextWindow ,
830- MaxOutputTokens : entry .MaxOutput ,
831- }
832- for _ , tool := range entry .ServerTools {
833- if tool != "" {
834- set .ServerTools [tool ] = CapabilitySupported
835- }
836- }
837- if len (set .ServerTools ) == 0 {
838- set .ServerTools = nil
839- }
840- for _ , feat := range entry .ServerTools {
841- switch strings .ToLower (strings .TrimSpace (feat )) {
842- case "function-calling" , "tools" :
843- set .FunctionCalling = CapabilitySupported
844- case "thinking:enabled" :
845- set .ExplicitThinkingBudget = CapabilitySupported
846- set .ThinkingTypes = append (set .ThinkingTypes , "enabled" )
847- case "thinking:adaptive" :
848- set .AdaptiveThinking = CapabilitySupported
849- set .ThinkingTypes = append (set .ThinkingTypes , "adaptive" )
850- case "effort" :
851- set .Effort = CapabilitySupported
852- case "structured_output" :
853- set .StructuredOutput = CapabilitySupported
854- case "code_execution" :
855- set .CodeExecution = CapabilitySupported
856- case "citations" :
857- set .Citations = CapabilitySupported
858- case "pdf_input" :
859- set .PDFInput = CapabilitySupported
860- case "image_input" :
861- set .ImageInput = CapabilitySupported
862- }
863- }
864- // Parse effort levels from features (format: "effort:low,medium,high")
865- for _ , feat := range entry .ServerTools {
866- if strings .HasPrefix (strings .ToLower (feat ), "effort:" ) {
867- levels := strings .TrimPrefix (strings .ToLower (feat ), "effort:" )
868- set .EffortLevels = strings .Split (levels , "," )
869- }
870- }
871- return set
872- }
873-
874- func pricingFromLegacy (entry ModelCatalogEntry , effectiveAt time.Time , source string ) PricingV1 {
875- in := entry .InputPricePer1M
876- out := entry .OutputPricePer1M
877- if in < 0 || out < 0 {
878- return PricingV1 {
879- Status : PricingUnknown ,
880- Currency : "USD" ,
881- EffectiveAt : effectiveAt ,
882- Source : source ,
883- }
884- }
885- pricing := PricingV1 {
886- Status : PricingKnown ,
887- Currency : "USD" ,
888- EffectiveAt : effectiveAt ,
889- RatesPer1M : map [string ]float64 {"input_tokens" : in , "output_tokens" : out },
890- Source : source ,
891- }
892- if in == 0 && out == 0 {
893- pricing .Status = PricingUnknown
894- pricing .RatesPer1M = nil
895- if strings .Contains (entry .ID , ":free" ) {
896- pricing .Status = PricingFree
897- pricing .RatesPer1M = map [string ]float64 {"input_tokens" : 0 , "output_tokens" : 0 }
898- }
899- }
900- return pricing
901- }
902-
903- // SanitizeCatalogV1Pricing drops invalid rate dimensions (e.g. negative OpenRouter prices).
904- func SanitizeCatalogV1Pricing (c * CatalogV1 ) {
905- if c == nil {
906- return
907- }
908- for i := range c .Offerings {
909- c .Offerings [i ].Pricing = sanitizePricingV1 (c .Offerings [i ].Pricing )
910- }
911- for i := range c .OfferingTemplates {
912- c .OfferingTemplates [i ].Pricing = sanitizePricingV1 (c .OfferingTemplates [i ].Pricing )
913- }
914- }
915-
916- func sanitizePricingV1 (p PricingV1 ) PricingV1 {
917- if len (p .RatesPer1M ) == 0 {
918- return p
919- }
920- clean := make (map [string ]float64 , len (p .RatesPer1M ))
921- for dim , rate := range p .RatesPer1M {
922- if dim == "" || rate < 0 {
923- continue
924- }
925- clean [dim ] = rate
926- }
927- if len (clean ) == 0 {
928- p .Status = PricingUnknown
929- p .RatesPer1M = nil
930- return p
931- }
932- p .RatesPer1M = clean
933- if p .Status == PricingKnown && (p .Currency == "" || len (p .RatesPer1M ) == 0 ) {
934- p .Status = PricingUnknown
935- p .RatesPer1M = nil
936- }
937- return p
938- }
939-
940- func uniqueNonEmpty (values ... string ) []string {
941- seen := map [string ]bool {}
942- var out []string
943- for _ , value := range values {
944- value = strings .TrimSpace (value )
945- if value == "" || seen [value ] {
946- continue
947- }
948- seen [value ] = true
949- out = append (out , value )
950- }
951- return out
952- }
953-
954- func looksCanonicalModelID (value string ) bool {
955- owner , model , ok := strings .Cut (value , "/" )
956- return ok && owner != "" && model != "" && ! strings .ContainsAny (value , " \t \r \n " )
957- }
958-
959- func validNativeModelIDSource (source NativeModelIDSource ) bool {
960- switch source {
961- case NativeModelIDCatalogKnown , NativeModelIDDiscovered , NativeModelIDUserConfigured , NativeModelIDCatalogOrUser :
962- return true
963- default :
964- return false
965- }
966- }
967-
968- func validatePricing (problems * []string , id string , pricing PricingV1 ) {
969- switch pricing .Status {
970- case PricingKnown , PricingPartial :
971- if pricing .Currency == "" || len (pricing .RatesPer1M ) == 0 {
972- * problems = append (* problems , fmt .Sprintf ("%s pricing is missing currency or rates" , id ))
973- }
974- case PricingUnknown :
975- if len (pricing .RatesPer1M ) > 0 {
976- * problems = append (* problems , fmt .Sprintf ("%s unknown pricing must not include rates" , id ))
977- }
978- case PricingFree :
979- if pricing .Currency == "" {
980- * problems = append (* problems , fmt .Sprintf ("%s free pricing missing currency" , id ))
981- }
982- default :
983- * problems = append (* problems , fmt .Sprintf ("%s invalid pricing status %q" , id , pricing .Status ))
984- }
985- for dim , rate := range pricing .RatesPer1M {
986- if dim == "" || rate < 0 {
987- * problems = append (* problems , fmt .Sprintf ("%s invalid pricing dimension %q" , id , dim ))
988- }
989- }
990- }
991-
992- func validateCapabilities (problems * []string , id string , capabilities CapabilitySetV1 ) {
993- valid := func (state CapabilityState ) bool {
994- return state == "" || state == CapabilitySupported || state == CapabilityUnsupported || state == CapabilityUnknown
995- }
996- if ! valid (capabilities .FunctionCalling ) {
997- * problems = append (* problems , fmt .Sprintf ("%s invalid function_calling capability" , id ))
998- }
999- if ! valid (capabilities .ExplicitThinkingBudget ) {
1000- * problems = append (* problems , fmt .Sprintf ("%s invalid explicit_thinking_budget capability" , id ))
1001- }
1002- for tool , state := range capabilities .ServerTools {
1003- if tool == "" || ! valid (state ) {
1004- * problems = append (* problems , fmt .Sprintf ("%s invalid server tool capability" , id ))
1005- }
1006- }
1007- }
1008-
1009- func cloneMap [T any ](in map [string ]T ) map [string ]T {
1010- out := make (map [string ]T , len (in ))
1011- for key , value := range in {
1012- out [key ] = value
1013- }
1014- return out
1015- }
0 commit comments