Implement a library profile API and built-in profile docs
This commit is contained in:
@@ -27,6 +27,7 @@ var (
|
||||
ErrInvalidRequest = errors.New("invalid run request")
|
||||
ErrProfileRequired = errors.New("profile selection is required")
|
||||
ErrAPIKeyEnvMissing = errors.New("api_key_env points to an unset environment variable")
|
||||
ErrAPIKeyRequired = errors.New("api key is required")
|
||||
ErrProfileLoad = errors.New("failed to load prompt definition")
|
||||
ErrArtifactLoad = errors.New("failed to load artifact")
|
||||
ErrPromptRender = errors.New("failed to render prompt")
|
||||
@@ -202,7 +203,7 @@ func (r *Runner) Prepare(ctx context.Context, req domain.RunRequest) (*domain.Pr
|
||||
if strings.TrimSpace(effectiveModel.Model) == "" {
|
||||
return nil, fmt.Errorf("%w: execution model is required", ErrInvalidRequest)
|
||||
}
|
||||
if err := validateAPIKey(effectiveModel.APIKeyEnv, effectiveModel.APIKey); err != nil {
|
||||
if err := validateAPIKey(effectiveModel.APIKeyEnv, effectiveModel.APIKey, effectiveModel.APIKeyRequired); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrInvalidRequest, err)
|
||||
}
|
||||
|
||||
@@ -363,6 +364,9 @@ func mergeExecutionTarget(base domain.ExecutionTarget, override domain.Execution
|
||||
if strings.TrimSpace(override.APIKeyEnv) != "" {
|
||||
out.APIKeyEnv = override.APIKeyEnv
|
||||
}
|
||||
if override.APIKeyRequired {
|
||||
out.APIKeyRequired = true
|
||||
}
|
||||
if len(override.ExtraParams) > 0 {
|
||||
out.ExtraParams = copyExtraParams(override.ExtraParams)
|
||||
}
|
||||
@@ -435,12 +439,15 @@ func resolveExecutionTarget(profileValue *domain.ExecutionProfile, override *dom
|
||||
return out, presence, nil
|
||||
}
|
||||
|
||||
func validateAPIKey(apiKeyEnv string, apiKey string) error {
|
||||
func validateAPIKey(apiKeyEnv string, apiKey string, apiKeyRequired bool) error {
|
||||
if strings.TrimSpace(apiKey) != "" {
|
||||
return nil
|
||||
}
|
||||
envName := strings.TrimSpace(apiKeyEnv)
|
||||
if envName == "" {
|
||||
if apiKeyRequired {
|
||||
return ErrAPIKeyRequired
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(os.Getenv(envName)) == "" {
|
||||
@@ -463,6 +470,7 @@ func executionProfileToTarget(p *domain.ExecutionProfile) domain.ExecutionTarget
|
||||
ServiceTier: p.ServiceTier,
|
||||
ReasoningEffort: p.ReasoningEffort,
|
||||
APIKeyEnv: p.APIKeyEnv,
|
||||
APIKeyRequired: p.APIKeyRequired,
|
||||
ExtraParams: copyExtraParams(p.ExtraParams),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1205,6 +1205,49 @@ func TestRunnerRunDirectAPIKeyBypassesMissingEnvAndReachesLLM(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerPrepareAPIKeyRequiredFailsWithoutDirectKey(t *testing.T) {
|
||||
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
||||
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
||||
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyRequired: true},
|
||||
}}
|
||||
runner := NewRunner(promptRepo, execRepo, defaultArtifactReader(), defaultRenderer(), &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}, nil)
|
||||
|
||||
_, err := runner.Prepare(context.Background(), domain.RunRequest{
|
||||
PromptID: "p",
|
||||
ProfileID: "exec",
|
||||
Inputs: singleInputRef(),
|
||||
})
|
||||
if !errors.Is(err, ErrAPIKeyRequired) {
|
||||
t.Fatalf("expected ErrAPIKeyRequired, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerRunAPIKeyRequiredSucceedsWithDirectKey(t *testing.T) {
|
||||
const directKey = "direct-required-key"
|
||||
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
||||
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
||||
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model", APIKeyRequired: true},
|
||||
}}
|
||||
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}
|
||||
runner := NewRunner(promptRepo, execRepo, defaultArtifactReader(), defaultRenderer(), llmClient, nil)
|
||||
|
||||
_, err := runner.Run(context.Background(), domain.RunRequest{
|
||||
PromptID: "p",
|
||||
ProfileID: "exec",
|
||||
APIKey: directKey,
|
||||
Inputs: singleInputRef(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if llmClient.lastReq.Target.APIKey != directKey {
|
||||
t.Fatalf("expected direct API key to reach LLM request")
|
||||
}
|
||||
if !llmClient.lastReq.Target.APIKeyRequired {
|
||||
t.Fatalf("expected APIKeyRequired to be carried to target")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerRunRuntimeAPIKeyEnvOverrideWorks(t *testing.T) {
|
||||
const envName = "SCRIPTORIUM_RUNTIME_API_KEY"
|
||||
t.Setenv(envName, "runtime-secret")
|
||||
@@ -1494,6 +1537,7 @@ func TestExecutionProfileToTargetPopulatesAllFieldsAndCopiesExtraParams(t *testi
|
||||
ServiceTier: "priority",
|
||||
ReasoningEffort: "medium",
|
||||
APIKeyEnv: "SCRIPTORIUM_API_KEY",
|
||||
APIKeyRequired: true,
|
||||
ExtraParams: map[string]any{
|
||||
"provider_option": "on",
|
||||
},
|
||||
@@ -1508,7 +1552,8 @@ func TestExecutionProfileToTargetPopulatesAllFieldsAndCopiesExtraParams(t *testi
|
||||
target.TimeoutSeconds != src.TimeoutSeconds ||
|
||||
target.ServiceTier != src.ServiceTier ||
|
||||
target.ReasoningEffort != src.ReasoningEffort ||
|
||||
target.APIKeyEnv != src.APIKeyEnv {
|
||||
target.APIKeyEnv != src.APIKeyEnv ||
|
||||
target.APIKeyRequired != src.APIKeyRequired {
|
||||
t.Fatalf("expected all profile fields to populate target, got %+v", target)
|
||||
}
|
||||
if !reflect.DeepEqual(target.ExtraParams, src.ExtraParams) {
|
||||
@@ -1533,6 +1578,7 @@ func TestResolveExecutionTargetProfileValuesPopulateAllSupportedFields(t *testin
|
||||
ServiceTier: "priority",
|
||||
ReasoningEffort: "low",
|
||||
APIKeyEnv: "PROFILE_KEY",
|
||||
APIKeyRequired: true,
|
||||
ExtraParams: map[string]any{
|
||||
"profile_option": "enabled",
|
||||
},
|
||||
@@ -1553,7 +1599,8 @@ func TestResolveExecutionTargetProfileValuesPopulateAllSupportedFields(t *testin
|
||||
target.TimeoutSeconds != profileValue.TimeoutSeconds ||
|
||||
target.ServiceTier != profileValue.ServiceTier ||
|
||||
target.ReasoningEffort != profileValue.ReasoningEffort ||
|
||||
target.APIKeyEnv != profileValue.APIKeyEnv {
|
||||
target.APIKeyEnv != profileValue.APIKeyEnv ||
|
||||
target.APIKeyRequired != profileValue.APIKeyRequired {
|
||||
t.Fatalf("expected profile values to populate target, got %+v", target)
|
||||
}
|
||||
if !reflect.DeepEqual(target.ExtraParams, profileValue.ExtraParams) {
|
||||
|
||||
Reference in New Issue
Block a user