diff --git a/cmd/root.go b/cmd/root.go index ed72d299..014f4ee1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/github/gh-models/cmd/list" "github.com/github/gh-models/cmd/run" + "github.com/github/gh-models/cmd/view" "github.com/spf13/cobra" ) @@ -14,6 +15,7 @@ func NewRootCommand() *cobra.Command { cmd.AddCommand(list.NewListCommand()) cmd.AddCommand(run.NewRunCommand()) + cmd.AddCommand(view.NewViewCommand()) return cmd } diff --git a/cmd/run/run.go b/cmd/run/run.go index 54dd4715..064b9148 100644 --- a/cmd/run/run.go +++ b/cmd/run/run.go @@ -234,7 +234,7 @@ func NewRunCommand() *cobra.Command { foundMatch := false for _, model := range models { - if strings.EqualFold(model.FriendlyName, modelName) || strings.EqualFold(model.Name, modelName) { + if model.HasName(modelName) { modelName = model.Name foundMatch = true break diff --git a/cmd/view/model_printer.go b/cmd/view/model_printer.go new file mode 100644 index 00000000..d3f20b7f --- /dev/null +++ b/cmd/view/model_printer.go @@ -0,0 +1,95 @@ +package view + +import ( + "strings" + + "github.com/cli/cli/v2/pkg/markdown" + "github.com/cli/go-gh/v2/pkg/tableprinter" + "github.com/cli/go-gh/v2/pkg/term" + "github.com/github/gh-models/internal/azure_models" + "github.com/mgutz/ansi" +) + +var ( + lightGrayUnderline = ansi.ColorFunc("white+du") +) + +type modelPrinter struct { + modelSummary *azure_models.ModelSummary + modelDetails *azure_models.ModelDetails + printer tableprinter.TablePrinter + terminalWidth int +} + +func newModelPrinter(summary *azure_models.ModelSummary, details *azure_models.ModelDetails, terminal term.Term) modelPrinter { + width, _, _ := terminal.Size() + printer := tableprinter.New(terminal.Out(), terminal.IsTerminalOutput(), width) + return modelPrinter{modelSummary: summary, modelDetails: details, printer: printer, terminalWidth: width} +} + +func (p *modelPrinter) render() error { + modelSummary := p.modelSummary + if modelSummary != nil { + p.printLabelledLine("Display name:", modelSummary.FriendlyName) + p.printLabelledLine("Summary name:", modelSummary.Name) + p.printLabelledLine("Publisher:", modelSummary.Publisher) + p.printLabelledLine("Summary:", modelSummary.Summary) + } + + modelDetails := p.modelDetails + if modelDetails != nil { + p.printLabelledLine("Context:", modelDetails.ContextLimits()) + p.printLabelledLine("Rate limit tier:", modelDetails.RateLimitTier) + p.printLabelledList("Tags:", modelDetails.Tags) + p.printLabelledList("Supported input types:", modelDetails.SupportedInputModalities) + p.printLabelledList("Supported output types:", modelDetails.SupportedOutputModalities) + p.printLabelledMultiLineList("Supported languages:", modelDetails.SupportedLanguages) + p.printLabelledLine("License:", modelDetails.License) + p.printMultipleLinesWithLabel("License description:", modelDetails.LicenseDescription) + p.printMultipleLinesWithLabel("Description:", modelDetails.Description) + p.printMultipleLinesWithLabel("Notes:", modelDetails.Notes) + p.printMultipleLinesWithLabel("Evaluation:", modelDetails.Evaluation) + } + + err := p.printer.Render() + if err != nil { + return err + } + + return nil +} + +func (p *modelPrinter) printLabelledLine(label string, value string) { + if value == "" { + return + } + p.addLabel(label) + p.printer.AddField(strings.TrimSpace(value)) + p.printer.EndRow() +} + +func (p *modelPrinter) printLabelledList(label string, values []string) { + p.printLabelledLine(label, strings.Join(values, ", ")) +} + +func (p *modelPrinter) printLabelledMultiLineList(label string, values []string) { + p.printMultipleLinesWithLabel(label, strings.Join(values, ", ")) +} + +func (p *modelPrinter) printMultipleLinesWithLabel(label string, value string) { + if value == "" { + return + } + p.addLabel(label) + renderedValue, err := markdown.Render(strings.TrimSpace(value), markdown.WithWrap(p.terminalWidth)) + displayValue := value + if err == nil { + displayValue = renderedValue + } + p.printer.AddField(displayValue, tableprinter.WithTruncate(nil)) + p.printer.EndRow() +} + +func (p *modelPrinter) addLabel(label string) { + p.printer.AddField(label, tableprinter.WithTruncate(nil), tableprinter.WithColor(lightGrayUnderline)) +} diff --git a/cmd/view/view.go b/cmd/view/view.go new file mode 100644 index 00000000..f36a051d --- /dev/null +++ b/cmd/view/view.go @@ -0,0 +1,94 @@ +package view + +import ( + "fmt" + "io" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/go-gh/v2/pkg/auth" + "github.com/cli/go-gh/v2/pkg/term" + "github.com/github/gh-models/internal/azure_models" + "github.com/github/gh-models/internal/ux" + "github.com/spf13/cobra" +) + +func NewViewCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "view [model]", + Short: "View details about a model", + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + terminal := term.FromEnv() + + token, _ := auth.TokenForHost("github.com") + if token == "" { + io.WriteString(terminal.Out(), "No GitHub token found. Please run 'gh auth login' to authenticate.\n") + return nil + } + + client := azure_models.NewClient(token) + + models, err := client.ListModels() + if err != nil { + return err + } + + ux.SortModels(models) + + modelName := "" + switch { + case len(args) == 0: + // Need to prompt for a model + prompt := &survey.Select{ + Message: "Select a model:", + Options: []string{}, + } + + for _, model := range models { + if !ux.IsChatModel(model) { + continue + } + prompt.Options = append(prompt.Options, model.FriendlyName) + } + + err = survey.AskOne(prompt, &modelName, survey.WithPageSize(10)) + if err != nil { + return err + } + + case len(args) >= 1: + modelName = args[0] + } + + modelSummary, err := getModelByName(modelName, models) + if err != nil { + return err + } + + modelDetails, err := client.GetModelDetails(modelSummary.RegistryName, modelSummary.Name, modelSummary.Version) + if err != nil { + return err + } + + modelPrinter := newModelPrinter(modelSummary, modelDetails, terminal) + + err = modelPrinter.render() + if err != nil { + return err + } + + return nil + }, + } + return cmd +} + +// getModelByName returns the model with the specified name, or an error if no such model exists within the given list. +func getModelByName(modelName string, models []*azure_models.ModelSummary) (*azure_models.ModelSummary, error) { + for _, model := range models { + if model.HasName(modelName) { + return model, nil + } + } + return nil, fmt.Errorf("the specified model name is not supported: %s", modelName) +} diff --git a/go.mod b/go.mod index 3078407d..27a21d5a 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,33 @@ module github.com/github/gh-models -go 1.22 +go 1.22.5 + +toolchain go1.22.8 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/briandowns/spinner v1.23.1 - github.com/cli/go-gh/v2 v2.9.0 - github.com/spf13/cobra v1.8.0 + github.com/cli/cli/v2 v2.58.0 + github.com/cli/go-gh/v2 v2.10.0 + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 + golang.org/x/text v0.18.0 ) require ( + github.com/alecthomas/chroma/v2 v2.8.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/glamour v0.7.0 // indirect github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c // indirect github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f // indirect - github.com/cli/safeexec v1.0.0 // indirect + github.com/cli/safeexec v1.0.1 // indirect github.com/cli/shurcooL-graphql v0.0.4 // indirect - github.com/fatih/color v1.7.0 // indirect - github.com/henvic/httpretty v0.0.6 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/henvic/httpretty v0.1.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/text v0.2.0 // indirect @@ -25,13 +35,16 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + github.com/yuin/goldmark v1.5.4 // indirect + github.com/yuin/goldmark-emoji v1.0.2 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 040efe9e..ca0b25dd 100644 --- a/go.sum +++ b/go.sum @@ -4,33 +4,53 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= +github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= +github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c h1:0FwZb0wTiyalb8QQlILWyIuh3nF5wok6j9D9oUQwfQY= github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs= github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f h1:1BXkZqDueTOBECyDoFGRi0xMYgjJ6vvoPIkWyKOwzTc= github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0= -github.com/cli/go-gh/v2 v2.9.0 h1:D3lTjEneMYl54M+WjZ+kRPrR5CEJ5BHS05isBPOV3LI= -github.com/cli/go-gh/v2 v2.9.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= -github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= -github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cli/cli/v2 v2.58.0 h1:sH4UIxaiFU794cV+XtVU8Etqk9Aw823v8+8JsEOd1EI= +github.com/cli/cli/v2 v2.58.0/go.mod h1:vTLS6aiHoA74ijRJ8TV7waYlAFhVWfxByilQymxpzJ4= +github.com/cli/go-gh/v2 v2.10.0 h1:GMflBKoErBXlLvN2euxzL+p7JaM8erlSmw0cT7uZr7M= +github.com/cli/go-gh/v2 v2.10.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= +github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= +github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= -github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= +github.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU= +github.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -50,40 +70,53 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= +github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= +github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= +github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -95,21 +128,23 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/internal/azure_models/client.go b/internal/azure_models/client.go index 2fcfbe5f..996c270a 100644 --- a/internal/azure_models/client.go +++ b/internal/azure_models/client.go @@ -4,12 +4,15 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "io" "net/http" "strings" "github.com/cli/go-gh/v2/pkg/api" "github.com/github/gh-models/internal/sse" + "golang.org/x/text/language" + "golang.org/x/text/language/display" ) type Client struct { @@ -19,7 +22,8 @@ type Client struct { const ( prodInferenceURL = "https://models.inference.ai.azure.com/chat/completions" - prodModelsURL = "https://api.catalog.azureml.ms/asset-gallery/v1.0/models" + azureAiStudioURL = "https://api.catalog.azureml.ms" + prodModelsURL = azureAiStudioURL + "/asset-gallery/v1.0/models" ) func NewClient(authToken string) *Client { @@ -83,6 +87,81 @@ func (c *Client) GetChatCompletionStream(req ChatCompletionOptions) (*ChatComple return &chatCompletionResponse, nil } +func (c *Client) GetModelDetails(registry string, modelName string, version string) (*ModelDetails, error) { + url := fmt.Sprintf("%s/asset-gallery/v1.0/%s/models/%s/version/%s", azureAiStudioURL, registry, modelName, version) + httpReq, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.client.Do(httpReq) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, c.handleHTTPError(resp) + } + + decoder := json.NewDecoder(resp.Body) + decoder.UseNumber() + + var detailsResponse modelCatalogDetailsResponse + err = decoder.Decode(&detailsResponse) + if err != nil { + return nil, err + } + + modelDetails := &ModelDetails{ + Description: detailsResponse.Description, + License: detailsResponse.License, + LicenseDescription: detailsResponse.LicenseDescription, + Notes: detailsResponse.Notes, + Tags: lowercaseStrings(detailsResponse.Keywords), + Evaluation: detailsResponse.Evaluation, + } + + modelLimits := detailsResponse.ModelLimits + if modelLimits != nil { + modelDetails.SupportedInputModalities = modelLimits.SupportedInputModalities + modelDetails.SupportedOutputModalities = modelLimits.SupportedOutputModalities + modelDetails.SupportedLanguages = convertLanguageCodesToNames(modelLimits.SupportedLanguages) + + textLimits := modelLimits.TextLimits + if textLimits != nil { + modelDetails.MaxOutputTokens = textLimits.MaxOutputTokens + modelDetails.MaxInputTokens = textLimits.InputContextWindow + } + } + + playgroundLimits := detailsResponse.PlaygroundLimits + if playgroundLimits != nil { + modelDetails.RateLimitTier = playgroundLimits.RateLimitTier + } + + return modelDetails, nil +} + +func convertLanguageCodesToNames(input []string) []string { + output := make([]string, len(input)) + english := display.English.Languages() + for i, code := range input { + tag := language.MustParse(code) + output[i] = english.Name(tag) + } + return output +} + +func lowercaseStrings(input []string) []string { + output := make([]string, len(input)) + for i, s := range input { + output[i] = strings.ToLower(s) + } + return output +} + func (c *Client) ListModels() ([]*ModelSummary, error) { body := bytes.NewReader([]byte(` { @@ -135,6 +214,8 @@ func (c *Client) ListModels() ([]*ModelSummary, error) { Task: inferenceTask, Publisher: summary.Publisher, Summary: summary.Summary, + Version: summary.Version, + RegistryName: summary.RegistryName, }) } diff --git a/internal/azure_models/types.go b/internal/azure_models/types.go index c3f7acfb..de76846b 100644 --- a/internal/azure_models/types.go +++ b/internal/azure_models/types.go @@ -2,6 +2,8 @@ package azure_models import ( "encoding/json" + "fmt" + "strings" "github.com/github/gh-models/internal/sse" ) @@ -65,6 +67,7 @@ type modelCatalogSearchSummary struct { Popularity json.Number `json:"popularity"` Publisher string `json:"publisher"` RegistryName string `json:"registryName"` + Version string `json:"version"` Summary string `json:"summary"` } @@ -75,6 +78,64 @@ type ModelSummary struct { Task string `json:"task"` Publisher string `json:"publisher"` Summary string `json:"summary"` + Version string `json:"version"` + RegistryName string `json:"registry_name"` +} + +func (m *ModelSummary) HasName(name string) bool { + return strings.EqualFold(m.FriendlyName, name) || strings.EqualFold(m.Name, name) +} + +type modelCatalogDetailsResponse struct { + AssetID string `json:"assetId"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + Publisher string `json:"publisher"` + Version string `json:"version"` + RegistryName string `json:"registryName"` + Evaluation string `json:"evaluation"` + Summary string `json:"summary"` + Description string `json:"description"` + License string `json:"license"` + LicenseDescription string `json:"licenseDescription"` + Notes string `json:"notes"` + Keywords []string `json:"keywords"` + InferenceTasks []string `json:"inferenceTasks"` + FineTuningTasks []string `json:"fineTuningTasks"` + Labels []string `json:"labels"` + TradeRestricted bool `json:"tradeRestricted"` + CreatedTime string `json:"createdTime"` + PlaygroundLimits *struct { + RateLimitTier string `json:"rateLimitTier"` + } `json:"playgroundLimits"` + ModelLimits *struct { + SupportedLanguages []string `json:"supportedLanguages"` + TextLimits *struct { + MaxOutputTokens int `json:"maxOutputTokens"` + InputContextWindow int `json:"inputContextWindow"` + } `json:"textLimits"` + SupportedInputModalities []string `json:"supportedInputModalities"` + SupportedOutputModalities []string `json:"supportedOutputModalities"` + } `json:"modelLimits"` +} + +type ModelDetails struct { + Description string `json:"description"` + Evaluation string `json:"evaluation"` + License string `json:"license"` + LicenseDescription string `json:"license_description"` + Notes string `json:"notes"` + Tags []string `json:"tags"` + SupportedInputModalities []string `json:"supported_input_modalities"` + SupportedOutputModalities []string `json:"supported_output_modalities"` + SupportedLanguages []string `json:"supported_languages"` + MaxOutputTokens int `json:"max_output_tokens"` + MaxInputTokens int `json:"max_input_tokens"` + RateLimitTier string `json:"rateLimitTier"` +} + +func (m *ModelDetails) ContextLimits() string { + return fmt.Sprintf("up to %d input tokens and %d output tokens", m.MaxInputTokens, m.MaxOutputTokens) } func Ptr[T any](value T) *T {