Validate and compose provider endpoints

This commit is contained in:
2026-08-11 23:38:45 +00:00
parent c281f721bc
commit 3a43550f70
18 changed files with 448 additions and 84 deletions

View File

@@ -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)
}
}

View File

@@ -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 = ""

View File

@@ -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 {