Validate and compose provider endpoints
This commit is contained in:
@@ -5,7 +5,6 @@ package backend
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -109,10 +108,11 @@ func (r *Registry) CapacityPolicies() map[string]domain.BackendCapacityPolicy {
|
||||
}
|
||||
|
||||
func normalizeBackend(definition domain.Backend) (domain.Backend, error) {
|
||||
definition.Endpoint = strings.TrimSpace(definition.Endpoint)
|
||||
if err := validateEndpoint(definition.Endpoint); err != nil {
|
||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(definition.Endpoint)
|
||||
if err != nil {
|
||||
return domain.Backend{}, fmt.Errorf("backend %q endpoint: %w", definition.ID, err)
|
||||
}
|
||||
definition.Endpoint = endpoint
|
||||
|
||||
definition.APIKeyEnv = strings.TrimSpace(definition.APIKeyEnv)
|
||||
if definition.APIKeyEnv != "" && !environmentVariableName.MatchString(definition.APIKeyEnv) {
|
||||
@@ -182,31 +182,3 @@ func normalizeBackend(definition domain.Backend) (domain.Backend, error) {
|
||||
definition.ExtraParams = extraParams
|
||||
return definition, nil
|
||||
}
|
||||
|
||||
func validateEndpoint(endpoint string) error {
|
||||
if endpoint == "" {
|
||||
return errors.New("must not be blank")
|
||||
}
|
||||
if strings.Contains(endpoint, "#") {
|
||||
return errors.New("must not contain a fragment")
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a valid URL: %w", err)
|
||||
}
|
||||
scheme := strings.ToLower(parsed.Scheme)
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return errors.New("must use http or https")
|
||||
}
|
||||
if !parsed.IsAbs() || parsed.Hostname() == "" {
|
||||
return errors.New("must be absolute and include a host")
|
||||
}
|
||||
if parsed.User != nil {
|
||||
return errors.New("must not contain user information")
|
||||
}
|
||||
if parsed.RawQuery != "" || parsed.ForceQuery {
|
||||
return errors.New("must not contain a query string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
39
internal/domain/endpoint.go
Normal file
39
internal/domain/endpoint.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NormalizeOpenAICompatibleBaseEndpoint trims and validates a source-neutral
|
||||
// OpenAI-compatible provider base endpoint.
|
||||
func NormalizeOpenAICompatibleBaseEndpoint(endpoint string) (string, error) {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint == "" {
|
||||
return "", errors.New("endpoint must not be blank")
|
||||
}
|
||||
if strings.Contains(endpoint, "#") {
|
||||
return "", errors.New("endpoint must not contain a fragment")
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return "", errors.New("endpoint must be a valid URL")
|
||||
}
|
||||
parsed.Scheme = strings.ToLower(parsed.Scheme)
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return "", errors.New("endpoint must use http or https")
|
||||
}
|
||||
if !parsed.IsAbs() || parsed.Hostname() == "" {
|
||||
return "", errors.New("endpoint must be absolute and include a host")
|
||||
}
|
||||
if parsed.User != nil {
|
||||
return "", errors.New("endpoint must not contain user information")
|
||||
}
|
||||
if parsed.RawQuery != "" || parsed.ForceQuery {
|
||||
return "", errors.New("endpoint must not contain a query string")
|
||||
}
|
||||
|
||||
return parsed.String(), nil
|
||||
}
|
||||
47
internal/domain/endpoint_test.go
Normal file
47
internal/domain/endpoint_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizeOpenAICompatibleBaseEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "http host", endpoint: "http://provider.example", want: "http://provider.example"},
|
||||
{name: "https nested path and whitespace", endpoint: " HTTPS://provider.example/api/openai/v1 ", want: "https://provider.example/api/openai/v1"},
|
||||
{name: "IPv4 host and port", endpoint: "http://127.0.0.1:8080/v1", want: "http://127.0.0.1:8080/v1"},
|
||||
{name: "IPv6 host and port", endpoint: "https://[::1]:8443/v1", want: "https://[::1]:8443/v1"},
|
||||
{name: "repeated trailing slashes", endpoint: "https://provider.example/v1///", want: "https://provider.example/v1///"},
|
||||
{name: "blank", endpoint: " \t\n ", wantErr: true},
|
||||
{name: "relative path", endpoint: "/api/v1", wantErr: true},
|
||||
{name: "scheme relative", endpoint: "//provider.example/v1", wantErr: true},
|
||||
{name: "missing host", endpoint: "https:///v1", wantErr: true},
|
||||
{name: "unsupported scheme", endpoint: "ftp://provider.example/v1", wantErr: true},
|
||||
{name: "user information", endpoint: "https://user:secret@provider.example/v1", wantErr: true},
|
||||
{name: "query", endpoint: "https://provider.example/v1?mode=chat", wantErr: true},
|
||||
{name: "empty query", endpoint: "https://provider.example/v1?", wantErr: true},
|
||||
{name: "fragment", endpoint: "https://provider.example/v1#chat", wantErr: true},
|
||||
{name: "empty fragment", endpoint: "https://provider.example/v1#", wantErr: true},
|
||||
{name: "malformed URL", endpoint: "https://provider.example/%zz", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := NormalizeOpenAICompatibleBaseEndpoint(tc.endpoint)
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatalf("expected endpoint error, got %q", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("normalize endpoint: %v", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("normalized endpoint = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -51,9 +51,11 @@ type OpenAICompatibleClient struct {
|
||||
}
|
||||
|
||||
func NewOpenAICompatibleClient(cfg OpenAICompatibleConfig) (*OpenAICompatibleClient, error) {
|
||||
baseURL := strings.TrimSpace(cfg.BaseURL)
|
||||
if baseURL != "" {
|
||||
if _, err := url.ParseRequestURI(baseURL); err != nil {
|
||||
baseURL := ""
|
||||
if strings.TrimSpace(cfg.BaseURL) != "" {
|
||||
var err error
|
||||
baseURL, err = domain.NormalizeOpenAICompatibleBaseEndpoint(cfg.BaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid base URL: %v", ErrInvalidConfig, err)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +77,7 @@ func NewOpenAICompatibleClient(cfg OpenAICompatibleConfig) (*OpenAICompatibleCli
|
||||
}
|
||||
|
||||
return &OpenAICompatibleClient{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
baseURL: baseURL,
|
||||
defaultModel: cfg.Model,
|
||||
httpClient: client,
|
||||
}, nil
|
||||
@@ -86,14 +88,18 @@ func (c *OpenAICompatibleClient) Generate(ctx context.Context, req domain.Genera
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
|
||||
}
|
||||
|
||||
endpoint := strings.TrimSpace(req.Target.Endpoint)
|
||||
if endpoint == "" {
|
||||
endpoint = c.baseURL
|
||||
selectedEndpoint := req.Target.Endpoint
|
||||
if strings.TrimSpace(selectedEndpoint) == "" {
|
||||
selectedEndpoint = c.baseURL
|
||||
}
|
||||
if endpoint == "" {
|
||||
return nil, fmt.Errorf("%w: endpoint is required", ErrInvalidRequest)
|
||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(selectedEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid endpoint: %v", ErrInvalidRequest, err)
|
||||
}
|
||||
endpoint, err = url.JoinPath(endpoint, defaults.OpenAIChatCompletionsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid endpoint path: %v", ErrInvalidRequest, err)
|
||||
}
|
||||
endpoint = strings.TrimRight(endpoint, "/") + defaults.OpenAIChatCompletionsPath
|
||||
|
||||
wireReq, err := openAIChatRequestFromGenerateRequest(req, c.defaultModel)
|
||||
if err != nil {
|
||||
|
||||
@@ -64,14 +64,21 @@ func assertDeadlineNear(t *testing.T, deadline, before, after time.Time, duratio
|
||||
}
|
||||
|
||||
func TestNewOpenAICompatibleClientRejectsInvalidBaseURL(t *testing.T) {
|
||||
_, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "://invalid",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid configuration error")
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidConfig) {
|
||||
t.Fatalf("expected ErrInvalidConfig, got %v", err)
|
||||
for _, endpoint := range []string{
|
||||
"://invalid",
|
||||
"/v1",
|
||||
"https:///v1",
|
||||
"ftp://provider.example/v1",
|
||||
"https://user@provider.example/v1",
|
||||
"https://provider.example/v1?mode=chat",
|
||||
"https://provider.example/v1#chat",
|
||||
} {
|
||||
t.Run(endpoint, func(t *testing.T) {
|
||||
_, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{BaseURL: endpoint})
|
||||
if !errors.Is(err, ErrInvalidConfig) {
|
||||
t.Fatalf("expected ErrInvalidConfig, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1312,6 +1319,94 @@ func TestOpenAICompatibleClientAllowsEmptyConfiguredBaseURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleClientComposesCompletionURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
baseURL string
|
||||
wantURL string
|
||||
}{
|
||||
{name: "HTTP host", baseURL: "http://provider.example", wantURL: "http://provider.example/chat/completions"},
|
||||
{name: "HTTPS host", baseURL: "https://provider.example", wantURL: "https://provider.example/chat/completions"},
|
||||
{name: "nested path", baseURL: "https://provider.example/api/openai/v1", wantURL: "https://provider.example/api/openai/v1/chat/completions"},
|
||||
{name: "trailing slash", baseURL: "https://provider.example/v1/", wantURL: "https://provider.example/v1/chat/completions"},
|
||||
{name: "repeated trailing slashes", baseURL: " https://provider.example/api/v1/// ", wantURL: "https://provider.example/api/v1/chat/completions"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var selectedURL string
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: tc.baseURL,
|
||||
Model: "m",
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
selectedURL = req.URL.String()
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(`{"choices":[{"message":{"content":"ok"}}]}`)),
|
||||
Request: req,
|
||||
}, nil
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct client: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("generate: %v", err)
|
||||
}
|
||||
if selectedURL != tc.wantURL {
|
||||
t.Fatalf("selected URL = %q, want %q", selectedURL, tc.wantURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleClientRejectsInvalidSelectedEndpointBeforeTransport(t *testing.T) {
|
||||
invalidEndpoints := []string{
|
||||
"/v1",
|
||||
"https:///v1",
|
||||
"ftp://provider.example/v1",
|
||||
"https://user@provider.example/v1",
|
||||
"https://provider.example/v1?mode=chat",
|
||||
"https://provider.example/v1#chat",
|
||||
"https://sensitive-endpoint.example/%zz",
|
||||
}
|
||||
for _, endpoint := range invalidEndpoints {
|
||||
t.Run(endpoint, func(t *testing.T) {
|
||||
transportCalls := 0
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "https://configured.example/v1",
|
||||
Model: "m",
|
||||
HTTPClient: &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
||||
transportCalls++
|
||||
return nil, errors.New("transport must not be called")
|
||||
})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("construct client: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.Generate(context.Background(), domain.GenerateRequest{
|
||||
Prompt: domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hi"}}},
|
||||
Target: domain.ExecutionTarget{Endpoint: endpoint},
|
||||
})
|
||||
if !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), endpoint) {
|
||||
t.Fatalf("error exposed selected endpoint %q: %v", endpoint, err)
|
||||
}
|
||||
if transportCalls != 0 {
|
||||
t.Fatalf("transport calls = %d, want 0", transportCalls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAICompatibleClientRequiresEndpointWhenUnsetEverywhere(t *testing.T) {
|
||||
client, err := NewOpenAICompatibleClient(OpenAICompatibleConfig{
|
||||
BaseURL: "",
|
||||
|
||||
@@ -132,7 +132,7 @@ func loadProfile(ctx context.Context, fsys fs.FS, root string, id string) (*doma
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s: %v", ErrInvalidProfile, relPath, err)
|
||||
}
|
||||
if err := validateProfile(prof); err != nil {
|
||||
if err := normalizeAndValidateProfile(prof); err != nil {
|
||||
if errors.Is(err, ErrRawAPIKeyNotAllowed) {
|
||||
return nil, fmt.Errorf("%w: %s", err, relPath)
|
||||
}
|
||||
@@ -256,13 +256,21 @@ func requireYAMLStreamEnd(decoder *yaml.Decoder) error {
|
||||
return errors.New("profile file must contain exactly one YAML document")
|
||||
}
|
||||
|
||||
func validateProfile(p *domain.ExecutionProfile) error {
|
||||
func normalizeAndValidateProfile(p *domain.ExecutionProfile) error {
|
||||
if strings.TrimSpace(p.ID) == "" {
|
||||
return errors.New("id is required")
|
||||
}
|
||||
if strings.TrimSpace(p.BackendID) == "" && strings.TrimSpace(p.Endpoint) == "" {
|
||||
p.Endpoint = strings.TrimSpace(p.Endpoint)
|
||||
if strings.TrimSpace(p.BackendID) == "" && p.Endpoint == "" {
|
||||
return errors.New("backend or endpoint is required")
|
||||
}
|
||||
if p.Endpoint != "" {
|
||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(p.Endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Endpoint = endpoint
|
||||
}
|
||||
if strings.TrimSpace(p.Model) == "" {
|
||||
return errors.New("model is required")
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestFilesystemRepository_GetProfile(t *testing.T) {
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "backend only", connection: "backend: ' openrouter '", wantBackend: "openrouter"},
|
||||
{name: "endpoint only", connection: "endpoint: http://localhost:8000/v1", wantEndpoint: "http://localhost:8000/v1"},
|
||||
{name: "endpoint only", connection: "endpoint: ' https://localhost:8000/nested/v1 '", wantEndpoint: "https://localhost:8000/nested/v1"},
|
||||
{name: "both", connection: "backend: openrouter\nendpoint: http://localhost:8000/v1", wantBackend: "openrouter", wantEndpoint: "http://localhost:8000/v1"},
|
||||
{name: "neither", wantErr: true},
|
||||
{name: "blank backend", connection: "backend: ' '", wantErr: true},
|
||||
@@ -351,6 +351,43 @@ model: second
|
||||
})
|
||||
}
|
||||
|
||||
func TestProfileRepositoriesRejectInvalidEndpoints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
withBackend bool
|
||||
}{
|
||||
{name: "relative", endpoint: "/v1"},
|
||||
{name: "missing host", endpoint: "https:///v1"},
|
||||
{name: "unsupported scheme", endpoint: "ftp://provider.example/v1"},
|
||||
{name: "user information", endpoint: "https://user@provider.example/v1"},
|
||||
{name: "query", endpoint: "https://provider.example/v1?mode=chat"},
|
||||
{name: "fragment", endpoint: "https://provider.example/v1#chat"},
|
||||
{name: "backend with invalid override", endpoint: "/v1", withBackend: true},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
backend := ""
|
||||
if tc.withBackend {
|
||||
backend = "backend: openrouter\n"
|
||||
}
|
||||
repo := NewFSRepository(fstest.MapFS{
|
||||
"profiles/invalid.yaml": profileMapFile(fmt.Sprintf(
|
||||
"id: invalid-endpoint\nmodel: model\n%sendpoint: %q\n",
|
||||
backend,
|
||||
tc.endpoint,
|
||||
)),
|
||||
}, "profiles")
|
||||
|
||||
_, err := repo.GetProfile(context.Background(), "invalid-endpoint")
|
||||
if !errors.Is(err, ErrInvalidProfile) {
|
||||
t.Fatalf("expected ErrInvalidProfile, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileRepositoriesValidateExtraParams(t *testing.T) {
|
||||
const validProfile = `
|
||||
id: selected-profile
|
||||
|
||||
@@ -31,3 +31,53 @@ func TestRunnerPrepareExecutionRejectsInvalidExecutionSettings(t *testing.T) {
|
||||
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerPrepareExecutionValidatesAndNormalizesRequestEndpoints(t *testing.T) {
|
||||
newRunner := func() *Runner {
|
||||
return NewRunner(
|
||||
&fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)},
|
||||
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
||||
nil,
|
||||
defaultArtifactReader(),
|
||||
defaultRenderer(),
|
||||
&fakeLLM{forbid: true},
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
invalidEndpoints := []string{
|
||||
"/v1",
|
||||
"https:///v1",
|
||||
"ftp://provider.example/v1",
|
||||
"https://user@provider.example/v1",
|
||||
"https://provider.example/v1?mode=chat",
|
||||
"https://provider.example/v1#chat",
|
||||
}
|
||||
for _, endpoint := range invalidEndpoints {
|
||||
t.Run(endpoint, func(t *testing.T) {
|
||||
_, err := newRunner().PrepareExecution(context.Background(), domain.RunRequest{
|
||||
PromptID: "p",
|
||||
ProfileID: "exec",
|
||||
Inputs: singleInputRef(),
|
||||
Execution: &domain.ExecutionTargetOverride{Endpoint: endpoint},
|
||||
})
|
||||
if !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
prepared, err := newRunner().PrepareExecution(context.Background(), domain.RunRequest{
|
||||
PromptID: "p",
|
||||
ProfileID: "exec",
|
||||
Inputs: singleInputRef(),
|
||||
Execution: &domain.ExecutionTargetOverride{Endpoint: " https://provider.example/nested/v1 "},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("prepare normalized endpoint: %v", err)
|
||||
}
|
||||
if got := prepared.Details().EffectiveModelParams.Endpoint; got != "https://provider.example/nested/v1" {
|
||||
t.Fatalf("effective endpoint = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,14 +57,19 @@ func (r *Runner) resolveProfileSelection(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateResolvedExecutionTarget(target domain.ExecutionTarget) error {
|
||||
if strings.TrimSpace(target.Endpoint) == "" {
|
||||
return errors.New("execution endpoint is required")
|
||||
func normalizeResolvedExecutionTarget(target domain.ExecutionTarget) (domain.ExecutionTarget, error) {
|
||||
endpoint, err := domain.NormalizeOpenAICompatibleBaseEndpoint(target.Endpoint)
|
||||
if err != nil {
|
||||
return domain.ExecutionTarget{}, fmt.Errorf("execution endpoint: %w", err)
|
||||
}
|
||||
target.Endpoint = endpoint
|
||||
if strings.TrimSpace(target.Model) == "" {
|
||||
return errors.New("execution model is required")
|
||||
return domain.ExecutionTarget{}, errors.New("execution model is required")
|
||||
}
|
||||
return domain.ValidateExecutionTargetSettings(target)
|
||||
if err := domain.ValidateExecutionTargetSettings(target); err != nil {
|
||||
return domain.ExecutionTarget{}, err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// InspectProfile resolves one explicit profile without prompt or execution work.
|
||||
@@ -87,7 +92,8 @@ func (r *Runner) InspectProfile(
|
||||
return nil, err
|
||||
}
|
||||
target, _ := resolveExecutionTarget(selection.backend, selection.profile, nil)
|
||||
if err := validateResolvedExecutionTarget(target); err != nil {
|
||||
target, err = normalizeResolvedExecutionTarget(target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
|
||||
}
|
||||
target.APIKey = ""
|
||||
|
||||
@@ -317,7 +317,8 @@ func (r *Runner) resolvePreparation(
|
||||
|
||||
effectiveModel, targetPresence := resolveExecutionTarget(selection.backend, selection.profile, req.Execution)
|
||||
effectiveModel.APIKey = req.APIKey
|
||||
if err := validateResolvedExecutionTarget(effectiveModel); err != nil {
|
||||
effectiveModel, err = normalizeResolvedExecutionTarget(effectiveModel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrInvalidRequest, err)
|
||||
}
|
||||
if err := validateAPIKey(effectiveModel.APIKeyEnv, effectiveModel.APIKey, effectiveModel.APIKeyRequired); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user