From cfe09858370a50490458edcf063ea6c698d74ad4 Mon Sep 17 00:00:00 2001 From: Yuriy Hrytsyuk Date: Mon, 2 Feb 2026 16:50:35 +0200 Subject: [PATCH 1/3] Add GetLLMs request to drapi, refactor GetTemplates --- internal/drapi/get.go | 93 +++++++++++++++++++++++++++++++++++++ internal/drapi/llms.go | 79 +++++++++++++++++++++++++++++++ internal/drapi/templates.go | 53 ++++----------------- 3 files changed, 182 insertions(+), 43 deletions(-) create mode 100644 internal/drapi/get.go create mode 100644 internal/drapi/llms.go diff --git a/internal/drapi/get.go b/internal/drapi/get.go new file mode 100644 index 00000000..3ab3aff9 --- /dev/null +++ b/internal/drapi/get.go @@ -0,0 +1,93 @@ +// 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 ( + datarobotEndpoint string + token string +) + +func Get(url, info string) (*http.Response, error) { + var err error + + // memoize datarobotEndpoint and token to avoid extra VerifyToken() calls + if datarobotEndpoint == "" || token == "" { + datarobotEndpoint, err = config.GetEndpointURL(url) + if err != nil { + return nil, err + } + + token, err = config.GetAPIKey() + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(http.MethodGet, datarobotEndpoint, 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..9fbf1b8c --- /dev/null +++ b/internal/drapi/llms.go @@ -0,0 +1,79 @@ +// 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 + +type LLM struct { + Model string `json:"model"` + LlmID string `json:"llmId"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Provider string `json:"provider"` + 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"` + IsActive bool `json:"isActive"` + 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 := "/api/v2/genai/llmgw/catalog/?limit=100" + + var llmList LLMList + + for url != "" { + prevLLMs := llmList.LLMs + + err := GetJSON(url, "LLMs", &llmList) + if err != nil { + return nil, err + } + + llmList.LLMs = append(prevLLMs, llmList.LLMs...) + url = llmList.Next + } + + return &llmList, nil +} diff --git a/internal/drapi/templates.go b/internal/drapi/templates.go index 7cd8f34e..be5e3b84 100644 --- a/internal/drapi/templates.go +++ b/internal/drapi/templates.go @@ -15,16 +15,10 @@ package drapi import ( - "encoding/json" - "errors" "fmt" - "net/http" "sort" "strings" "time" - - "github.com/charmbracelet/log" - "github.com/datarobot/cli/internal/config" ) type Template struct { @@ -126,47 +120,20 @@ func (tl TemplateList) SortByName() TemplateList { } func GetTemplates() (*TemplateList, error) { - datarobotEndpoint, err := config.GetEndpointURL("/api/v2/applicationTemplates/") - 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 - } + url := "/api/v2/applicationTemplates/?limit=100" - req.Header.Add("Authorization", "Bearer "+token) - req.Header.Add("User-Agent", config.GetUserAgentHeader()) - - 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 - } - defer resp.Body.Close() + var templateList TemplateList - if resp.StatusCode != http.StatusOK { - return nil, errors.New("Response status code is " + resp.Status + ".") - } + for url != "" { + prevTemplates := templateList.Templates - var templateList TemplateList + err := GetJSON(url, "templates", &templateList) + if err != nil { + return nil, err + } - err = json.NewDecoder(resp.Body).Decode(&templateList) - if err != nil { - return nil, err + templateList.Templates = append(prevTemplates, templateList.Templates...) + url = templateList.Next } return &templateList, nil From 71ff7f05e53fcbbf1b6864a37a2e6066d1bba102 Mon Sep 17 00:00:00 2001 From: Yuriy Hrytsyuk Date: Tue, 3 Feb 2026 18:05:21 +0200 Subject: [PATCH 2/3] Add llmgw_catalog prompt type --- cmd/dotenv/promptModel.go | 24 ++++++++++++++++++++++++ internal/config/api.go | 2 +- internal/drapi/get.go | 16 ++++------------ internal/drapi/llms.go | 24 ++++++++++++++++++++---- internal/drapi/templates.go | 17 +++++++++++++---- 5 files changed, 62 insertions(+), 21 deletions(-) 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 index 3ab3aff9..506ef287 100644 --- a/internal/drapi/get.go +++ b/internal/drapi/get.go @@ -24,28 +24,20 @@ import ( "github.com/datarobot/cli/internal/config" ) -var ( - datarobotEndpoint string - token string -) +var token string func Get(url, info string) (*http.Response, error) { var err error - // memoize datarobotEndpoint and token to avoid extra VerifyToken() calls - if datarobotEndpoint == "" || token == "" { - datarobotEndpoint, err = config.GetEndpointURL(url) - if err != nil { - return nil, err - } - + // 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, datarobotEndpoint, nil) + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, err } diff --git a/internal/drapi/llms.go b/internal/drapi/llms.go index 9fbf1b8c..f103a780 100644 --- a/internal/drapi/llms.go +++ b/internal/drapi/llms.go @@ -14,6 +14,10 @@ package drapi +import ( + "github.com/datarobot/cli/internal/config" +) + type LLM struct { Model string `json:"model"` LlmID string `json:"llmId"` @@ -59,21 +63,33 @@ type LLMList struct { } func GetLLMs() (*LLMList, error) { - url := "/api/v2/genai/llmgw/catalog/?limit=100" + 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 != "" { - prevLLMs := llmList.LLMs + llmList = LLMList{} - err := GetJSON(url, "LLMs", &llmList) + err = GetJSON(url, "LLMs", &llmList) if err != nil { return nil, err } - llmList.LLMs = append(prevLLMs, llmList.LLMs...) + 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 be5e3b84..81343ea7 100644 --- a/internal/drapi/templates.go +++ b/internal/drapi/templates.go @@ -19,6 +19,8 @@ import ( "sort" "strings" "time" + + "github.com/datarobot/cli/internal/config" ) type Template struct { @@ -120,22 +122,29 @@ func (tl TemplateList) SortByName() TemplateList { } func GetTemplates() (*TemplateList, error) { - url := "/api/v2/applicationTemplates/?limit=100" + url, err := config.GetEndpointURL("/api/v2/applicationTemplates/?limit=100") + if err != nil { + return nil, err + } var templateList TemplateList + var templates []Template + for url != "" { - prevTemplates := templateList.Templates + templateList = TemplateList{} - err := GetJSON(url, "templates", &templateList) + err = GetJSON(url, "templates", &templateList) if err != nil { return nil, err } - templateList.Templates = append(prevTemplates, templateList.Templates...) + templates = append(templates, templateList.Templates...) url = templateList.Next } + templateList.Templates = templates + return &templateList, nil } From 967fba7e8f9ef73647d32c8dbe47c68cad287d6b Mon Sep 17 00:00:00 2001 From: Yuriy Hrytsyuk Date: Tue, 3 Feb 2026 18:20:31 +0200 Subject: [PATCH 3/3] Cleanup --- internal/drapi/llms.go | 65 +++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/internal/drapi/llms.go b/internal/drapi/llms.go index f103a780..80eef680 100644 --- a/internal/drapi/llms.go +++ b/internal/drapi/llms.go @@ -19,39 +19,40 @@ import ( ) type LLM struct { - Model string `json:"model"` - LlmID string `json:"llmId"` - Name string `json:"name"` - Version string `json:"version"` - Description string `json:"description"` - Provider string `json:"provider"` - 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"` - IsActive bool `json:"isActive"` - AvailableRegions []string `json:"availableRegions"` + LlmID string `json:"llmId"` + Name string `json:"name"` + Provider string `json:"provider"` + IsActive bool `json:"isActive"` - ReferenceLinks []struct { - Name string `json:"name"` - URL string `json:"url"` - } `json:"referenceLinks"` - - AvailableLitellmEndpoints struct { - SupportsChatCompletions bool `json:"supportsChatCompletions"` - SupportsResponses bool `json:"supportsResponses"` - } `json:"availableLitellmEndpoints"` + //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 {