diff --git a/cmd/dotenv/promptModel.go b/cmd/dotenv/promptModel.go index d547eb46..fa64381e 100644 --- a/cmd/dotenv/promptModel.go +++ b/cmd/dotenv/promptModel.go @@ -24,6 +24,7 @@ import ( "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/datarobot/cli/internal/drapi" "github.com/datarobot/cli/internal/envbuilder" "github.com/datarobot/cli/tui" ) @@ -92,6 +93,10 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list } func newPromptModel(prompt envbuilder.UserPrompt, successCmd tea.Cmd) (promptModel, tea.Cmd) { + if prompt.Type == "llmgw_catalog" { + return newLLMListPrompt(prompt, successCmd) + } + if len(prompt.Options) == 0 { return newTextInputPrompt(prompt, successCmd) } @@ -170,6 +175,25 @@ func newListPrompt(prompt envbuilder.UserPrompt, successCmd tea.Cmd) (promptMode }, cmd } +func newLLMListPrompt(prompt envbuilder.UserPrompt, successCmd tea.Cmd) (promptModel, tea.Cmd) { + llms, err := drapi.GetLLMs() + if err != nil { + return promptModel{}, nil + } + + for _, llm := range llms.LLMs { + prompt.Options = append(prompt.Options, envbuilder.PromptOption{ + Blank: false, + Checked: false, + Name: fmt.Sprintf("%s (%s)", llm.Name, llm.Provider), + Value: llm.LlmID, + Requires: "", + }) + } + + return newListPrompt(prompt, successCmd) +} + func (pm promptModel) GetValues() []string { if len(pm.prompt.Options) == 0 { return []string{strings.TrimSpace(pm.input.Value())} diff --git a/internal/config/api.go b/internal/config/api.go index 9b61ebd3..4dfd1288 100644 --- a/internal/config/api.go +++ b/internal/config/api.go @@ -60,7 +60,7 @@ func GetEndpointURL(endpoint string) (string, error) { return "", errors.New("Empty URL.") } - return url.JoinPath(baseURL, endpoint) + return baseURL + endpoint, nil } func GetUserAgentHeader() string { diff --git a/internal/drapi/get.go b/internal/drapi/get.go new file mode 100644 index 00000000..506ef287 --- /dev/null +++ b/internal/drapi/get.go @@ -0,0 +1,85 @@ +// Copyright 2025 DataRobot, Inc. and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package drapi + +import ( + "encoding/json" + "errors" + "net/http" + "time" + + "github.com/charmbracelet/log" + "github.com/datarobot/cli/internal/config" +) + +var token string + +func Get(url, info string) (*http.Response, error) { + var err error + + // memoize token to avoid extra VerifyToken() calls + if token == "" { + token, err = config.GetAPIKey() + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", "Bearer "+token) + req.Header.Add("User-Agent", config.GetUserAgentHeader()) + + if info != "" { + log.Infof("Fetching %s from: %s", info, url) + } + + log.Debug("Request Info: \n" + config.RedactedReqInfo(req)) + + client := &http.Client{ + Timeout: 30 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, errors.New("Response status code is " + resp.Status + ".") + } + + return resp, err +} + +func GetJSON(url, info string, v any) error { + resp, err := Get(url, info) + if err != nil { + return err + } + + err = json.NewDecoder(resp.Body).Decode(&v) + if err != nil { + return err + } + + resp.Body.Close() + + return nil +} diff --git a/internal/drapi/llms.go b/internal/drapi/llms.go new file mode 100644 index 00000000..80eef680 --- /dev/null +++ b/internal/drapi/llms.go @@ -0,0 +1,96 @@ +// Copyright 2025 DataRobot, Inc. and its affiliates. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package drapi + +import ( + "github.com/datarobot/cli/internal/config" +) + +type LLM struct { + LlmID string `json:"llmId"` + Name string `json:"name"` + Provider string `json:"provider"` + IsActive bool `json:"isActive"` + + //Model string `json:"model"` + //Version string `json:"version"` + //Description string `json:"description"` + //Creator string `json:"creator"` + //ContextSize int `json:"contextSize"` + //MaxCompletionTokens int `json:"maxCompletionTokens"` + //Capabilities []string `json:"capabilities"` + //SupportedLanguages []string `json:"supportedLanguages"` + //InputTypes []string `json:"inputTypes"` + //OutputTypes []string `json:"outputTypes"` + //DocumentationLink string `json:"documentationLink"` + //DateAdded string `json:"dateAdded"` + //License string `json:"license"` + //IsPreview bool `json:"isPreview"` + //IsMetered bool `json:"isMetered"` + //RetirementDate string `json:"retirementDate"` + //SuggestedReplacement string `json:"suggestedReplacement"` + //IsDeprecated bool `json:"isDeprecated"` + //AvailableRegions []string `json:"availableRegions"` + // + //ReferenceLinks []struct { + // Name string `json:"name"` + // URL string `json:"url"` + //} `json:"referenceLinks"` + // + //AvailableLitellmEndpoints struct { + // SupportsChatCompletions bool `json:"supportsChatCompletions"` + // SupportsResponses bool `json:"supportsResponses"` + //} `json:"availableLitellmEndpoints"` +} + +type LLMList struct { + LLMs []LLM `json:"data"` + Count int `json:"count"` + TotalCount int `json:"totalCount"` + Next string `json:"next"` + Previous string `json:"previous"` +} + +func GetLLMs() (*LLMList, error) { + url, err := config.GetEndpointURL("/api/v2/genai/llmgw/catalog/?limit=100") + if err != nil { + return nil, err + } + + var llmList LLMList + + var active []LLM + + for url != "" { + llmList = LLMList{} + + err = GetJSON(url, "LLMs", &llmList) + if err != nil { + return nil, err + } + + for _, llm := range llmList.LLMs { + if llm.IsActive { + active = append(active, llm) + } + } + + url = llmList.Next + } + + llmList.LLMs = active + + return &llmList, nil +} diff --git a/internal/drapi/templates.go b/internal/drapi/templates.go index 7cd8f34e..81343ea7 100644 --- a/internal/drapi/templates.go +++ b/internal/drapi/templates.go @@ -15,15 +15,11 @@ package drapi import ( - "encoding/json" - "errors" "fmt" - "net/http" "sort" "strings" "time" - "github.com/charmbracelet/log" "github.com/datarobot/cli/internal/config" ) @@ -126,48 +122,28 @@ func (tl TemplateList) SortByName() TemplateList { } func GetTemplates() (*TemplateList, error) { - datarobotEndpoint, err := config.GetEndpointURL("/api/v2/applicationTemplates/") + url, err := config.GetEndpointURL("/api/v2/applicationTemplates/?limit=100") if err != nil { return nil, err } - log.Info("Fetching templates from " + datarobotEndpoint) - - req, err := http.NewRequest(http.MethodGet, datarobotEndpoint+"?limit=100", nil) - if err != nil { - return nil, err - } - - token, err := config.GetAPIKey() - if err != nil { - return nil, err - } - - req.Header.Add("Authorization", "Bearer "+token) - req.Header.Add("User-Agent", config.GetUserAgentHeader()) + var templateList TemplateList - log.Debug("Request Info: \n" + config.RedactedReqInfo(req)) + var templates []Template - client := &http.Client{ - Timeout: 30 * time.Second, - } + for url != "" { + templateList = TemplateList{} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() + err = GetJSON(url, "templates", &templateList) + if err != nil { + return nil, err + } - if resp.StatusCode != http.StatusOK { - return nil, errors.New("Response status code is " + resp.Status + ".") + templates = append(templates, templateList.Templates...) + url = templateList.Next } - var templateList TemplateList - - err = json.NewDecoder(resp.Body).Decode(&templateList) - if err != nil { - return nil, err - } + templateList.Templates = templates return &templateList, nil }