Add session and reasoning controls
This commit is contained in:
@@ -32,33 +32,36 @@ const (
|
||||
type runConfig struct {
|
||||
configPath string
|
||||
|
||||
promptDir string
|
||||
profileDir string
|
||||
promptID string
|
||||
promptVersion string
|
||||
profileID string
|
||||
inputRaw listFlag
|
||||
varRaw listFlag
|
||||
outputPath string
|
||||
llmBaseURL string
|
||||
apiKeyEnv string
|
||||
model string
|
||||
temperature float64
|
||||
maxTokens int
|
||||
topP float64
|
||||
schemaDir string
|
||||
backends []appconfig.BackendSettings
|
||||
timeout time.Duration
|
||||
promptDir string
|
||||
profileDir string
|
||||
promptID string
|
||||
promptVersion string
|
||||
profileID string
|
||||
sessionID string
|
||||
inputRaw listFlag
|
||||
varRaw listFlag
|
||||
outputPath string
|
||||
llmBaseURL string
|
||||
apiKeyEnv string
|
||||
model string
|
||||
temperature float64
|
||||
maxTokens int
|
||||
topP float64
|
||||
reasoningEffort string
|
||||
schemaDir string
|
||||
backends []appconfig.BackendSettings
|
||||
timeout time.Duration
|
||||
|
||||
defaultRenderFormat renderformat.PreparedRunOutputFormat
|
||||
|
||||
llmBaseURLSet bool
|
||||
apiKeyEnvSet bool
|
||||
modelSet bool
|
||||
temperatureSet bool
|
||||
maxTokensSet bool
|
||||
topPSet bool
|
||||
timeoutSet bool
|
||||
llmBaseURLSet bool
|
||||
apiKeyEnvSet bool
|
||||
modelSet bool
|
||||
temperatureSet bool
|
||||
maxTokensSet bool
|
||||
topPSet bool
|
||||
reasoningEffortSet bool
|
||||
timeoutSet bool
|
||||
}
|
||||
|
||||
type renderConfig struct {
|
||||
@@ -352,6 +355,7 @@ func registerExecutionRequestFlags(fs *flag.FlagSet, cfg *runConfig) {
|
||||
fs.StringVar(&cfg.promptID, "prompt", "", "prompt ID to run")
|
||||
fs.StringVar(&cfg.promptVersion, "prompt-version", "", "optional prompt definition version")
|
||||
fs.StringVar(&cfg.profileID, "profile", "", "optional execution profile ID; if omitted, prompt default_profile is used")
|
||||
fs.StringVar(&cfg.sessionID, "session-id", "", "optional session ID")
|
||||
fs.Var(&cfg.inputRaw, "input", "input mapping(s): name=path (repeatable, comma-separated)")
|
||||
fs.Var(&cfg.varRaw, "var", "variable mapping(s): name=value (repeatable, comma-separated)")
|
||||
fs.StringVar(&cfg.outputPath, "out", "", "optional output file path")
|
||||
@@ -361,6 +365,7 @@ func registerExecutionRequestFlags(fs *flag.FlagSet, cfg *runConfig) {
|
||||
fs.Float64Var(&cfg.temperature, "temperature", 0, "optional temperature override")
|
||||
fs.IntVar(&cfg.maxTokens, "max-tokens", 0, "optional max tokens override")
|
||||
fs.Float64Var(&cfg.topP, "top-p", 0, "optional top_p override")
|
||||
fs.StringVar(&cfg.reasoningEffort, "reasoning-effort", "", "optional reasoning effort override")
|
||||
fs.DurationVar(&cfg.timeout, "timeout", 0, "LLM request timeout")
|
||||
fs.StringVar(&cfg.promptID, "prompt-id", "", "deprecated alias for --prompt")
|
||||
fs.StringVar(&cfg.profileID, "profile-id", "", "deprecated alias for --profile")
|
||||
@@ -405,6 +410,7 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
|
||||
cfg.temperatureSet = flagWasSet(fs, "temperature")
|
||||
cfg.maxTokensSet = flagWasSet(fs, "max-tokens")
|
||||
cfg.topPSet = flagWasSet(fs, "top-p")
|
||||
cfg.reasoningEffortSet = flagWasSet(fs, "reasoning-effort")
|
||||
cfg.timeoutSet = flagWasSet(fs, "timeout")
|
||||
return nil
|
||||
}
|
||||
@@ -605,7 +611,7 @@ func buildRunRequestFromConfig(cfg *runConfig) (promptkit.RunRequest, error) {
|
||||
}
|
||||
|
||||
var modelOverride *promptkit.ExecutionTargetOverride
|
||||
if cfg.llmBaseURLSet || cfg.modelSet || cfg.temperatureSet || cfg.maxTokensSet || cfg.topPSet || cfg.apiKeyEnvSet || cfg.timeoutSet {
|
||||
if cfg.llmBaseURLSet || cfg.modelSet || cfg.temperatureSet || cfg.maxTokensSet || cfg.topPSet || cfg.reasoningEffortSet || cfg.apiKeyEnvSet || cfg.timeoutSet {
|
||||
modelOverride = &promptkit.ExecutionTargetOverride{
|
||||
Endpoint: cfg.llmBaseURL,
|
||||
Model: cfg.model,
|
||||
@@ -620,6 +626,9 @@ func buildRunRequestFromConfig(cfg *runConfig) (promptkit.RunRequest, error) {
|
||||
if cfg.topPSet {
|
||||
modelOverride.TopP = &cfg.topP
|
||||
}
|
||||
if cfg.reasoningEffortSet {
|
||||
modelOverride.ReasoningEffort = &cfg.reasoningEffort
|
||||
}
|
||||
if cfg.timeoutSet {
|
||||
timeoutSeconds := int(cfg.timeout.Seconds())
|
||||
modelOverride.TimeoutSeconds = &timeoutSeconds
|
||||
@@ -630,6 +639,7 @@ func buildRunRequestFromConfig(cfg *runConfig) (promptkit.RunRequest, error) {
|
||||
PromptID: cfg.promptID,
|
||||
PromptVersion: cfg.promptVersion,
|
||||
ProfileID: cfg.profileID,
|
||||
SessionID: cfg.sessionID,
|
||||
Inputs: inputs,
|
||||
Vars: varMappings,
|
||||
Execution: modelOverride,
|
||||
@@ -727,7 +737,7 @@ func printSummary(stderr io.Writer, res *promptkit.RunResult) {
|
||||
|
||||
func printUsage(w io.Writer) {
|
||||
fmt.Fprintln(w, "usage: scriptorium <run|render|serve> ...")
|
||||
fmt.Fprintln(w, " run: scriptorium run [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--out path] [--timeout 10m]")
|
||||
fmt.Fprintln(w, " render: scriptorium render [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--format text|json] [--out path] [--timeout 10m]")
|
||||
fmt.Fprintln(w, " run: scriptorium run [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--session-id ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--reasoning-effort VALUE] [--var k=v] [--out path] [--timeout 10m]")
|
||||
fmt.Fprintln(w, " render: scriptorium render [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--session-id ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--reasoning-effort VALUE] [--var k=v] [--format text|json] [--out path] [--timeout 10m]")
|
||||
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] [--artifact-root DIR] [--max-request-bytes N] [--max-artifact-bytes N] [--max-response-bytes N]\n", defaults.HTTPAddrDefault)
|
||||
}
|
||||
|
||||
@@ -103,12 +103,14 @@ func TestParseRunArgsFlagMapping(t *testing.T) {
|
||||
"--prompt", "prompt.a",
|
||||
"--prompt-version", "2",
|
||||
"--profile", "profile.a",
|
||||
"--session-id", "session-1",
|
||||
"--input", "a=b",
|
||||
"--llm-base-url", "http://x/v1",
|
||||
"--model", "m",
|
||||
"--temperature", "0.7",
|
||||
"--max-tokens", "111",
|
||||
"--top-p", "0.8",
|
||||
"--reasoning-effort", "medium",
|
||||
"--timeout", "30s",
|
||||
"--api-key-env", "SCRIPTORIUM_API_KEY",
|
||||
})
|
||||
@@ -121,6 +123,9 @@ func TestParseRunArgsFlagMapping(t *testing.T) {
|
||||
if cfg.promptID != "prompt.a" || cfg.promptVersion != "2" || cfg.profileID != "profile.a" {
|
||||
t.Fatalf("unexpected prompt/version/profile ids: %q %q %q", cfg.promptID, cfg.promptVersion, cfg.profileID)
|
||||
}
|
||||
if cfg.sessionID != "session-1" || cfg.reasoningEffort != "medium" || !cfg.reasoningEffortSet {
|
||||
t.Fatalf("unexpected session or reasoning configuration: %+v", cfg)
|
||||
}
|
||||
if !cfg.llmBaseURLSet || !cfg.modelSet || !cfg.temperatureSet || !cfg.maxTokensSet || !cfg.topPSet || !cfg.timeoutSet || !cfg.apiKeyEnvSet {
|
||||
t.Fatalf("expected override flags set, got %+v", cfg)
|
||||
}
|
||||
@@ -206,6 +211,8 @@ func TestUsageIncludesExecutionAndServeFlags(t *testing.T) {
|
||||
usage := stderr.String()
|
||||
for _, want := range []string{
|
||||
"--prompt-version VERSION",
|
||||
"--session-id ID",
|
||||
"--reasoning-effort VALUE",
|
||||
"--artifact-root",
|
||||
"--max-request-bytes",
|
||||
"--max-artifact-bytes",
|
||||
@@ -694,6 +701,39 @@ func TestBuildRunRequestPreservesNumericOverridePresence(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRunRequestPreservesReasoningEffortPresenceAndSessionID(t *testing.T) {
|
||||
omitted, err := buildRunRequestFromConfig(&runConfig{promptID: "prompt-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected omitted request to build, got %v", err)
|
||||
}
|
||||
if omitted.Execution != nil {
|
||||
t.Fatalf("expected omitted reasoning flag to leave execution nil, got %#v", omitted.Execution)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
value string
|
||||
}{
|
||||
{name: "replacement", value: "high"},
|
||||
{name: "clear", value: ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req, err := buildRunRequestFromConfig(&runConfig{
|
||||
promptID: "prompt-1",
|
||||
sessionID: "session-1",
|
||||
reasoningEffort: tc.value,
|
||||
reasoningEffortSet: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected request to build, got %v", err)
|
||||
}
|
||||
if req.SessionID != "session-1" || req.Execution == nil || req.Execution.ReasoningEffort == nil || *req.Execution.ReasoningEffort != tc.value {
|
||||
t.Fatalf("unexpected mapped request: %#v", req)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRunRequestAllowsOmittedInputsAndMapsPromptVersion(t *testing.T) {
|
||||
req, err := buildRunRequestFromConfig(&runConfig{
|
||||
promptID: "prompt-1",
|
||||
@@ -1002,6 +1042,118 @@ backends:
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCommandMapsReasoningEffortAndSessionID(t *testing.T) {
|
||||
lib := newCLITestLibrary(t)
|
||||
writePromptDefinition(t, lib.promptDir, "session.yaml", `id: session
|
||||
version: "1"
|
||||
default_profile: local
|
||||
session_id: definition-session
|
||||
messages:
|
||||
- role: user
|
||||
content: "hello"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`)
|
||||
if err := os.WriteFile(filepath.Join(lib.profileDir, "local.yaml"), []byte(`id: local
|
||||
endpoint: http://127.0.0.1:1/v1
|
||||
model: local-model
|
||||
reasoning_effort: low
|
||||
`), 0o644); err != nil {
|
||||
t.Fatalf("write profile fixture: %v", err)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
wantReasoning string
|
||||
wantSessionID string
|
||||
absentReasoning bool
|
||||
}{
|
||||
{name: "omitted reasoning inherits profile", wantReasoning: "low", wantSessionID: "definition-session"},
|
||||
{name: "nonblank reasoning replaces profile", args: []string{"--reasoning-effort", "high"}, wantReasoning: "high", wantSessionID: "definition-session"},
|
||||
{name: "empty reasoning clears profile", args: []string{"--reasoning-effort="}, wantSessionID: "definition-session", absentReasoning: true},
|
||||
{name: "direct session replaces definition", args: []string{"--session-id", "direct-session"}, wantReasoning: "low", wantSessionID: "direct-session"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
args := []string{"--prompt-dir", lib.promptDir, "--profile-dir", lib.profileDir, "--prompt", "session"}
|
||||
args = append(args, tc.args...)
|
||||
code, stdout, stderr := runCLICommand(t, renderCommand, args)
|
||||
if code != ExitOK {
|
||||
t.Fatalf("expected ExitOK, got %d stderr=%q", code, stderr)
|
||||
}
|
||||
if !strings.Contains(stdout, "session_id: "+tc.wantSessionID) {
|
||||
t.Fatalf("expected session ID %q, got:\n%s", tc.wantSessionID, stdout)
|
||||
}
|
||||
hasReasoning := strings.Contains(stdout, "reasoning_effort:")
|
||||
if tc.absentReasoning {
|
||||
if hasReasoning {
|
||||
t.Fatalf("expected cleared reasoning to be omitted, got:\n%s", stdout)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !strings.Contains(stdout, "reasoning_effort: "+tc.wantReasoning) {
|
||||
t.Fatalf("expected reasoning effort %q, got:\n%s", tc.wantReasoning, stdout)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCommandOmitsEmptyEffectiveSessionID(t *testing.T) {
|
||||
lib := newCLITestLibrary(t)
|
||||
writePromptDefinition(t, lib.promptDir, "plain.yaml", `id: plain
|
||||
version: "1"
|
||||
default_profile: local
|
||||
messages:
|
||||
- role: user
|
||||
content: "hello"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`)
|
||||
writeProfileFile(t, lib.profileDir, "local", "http://127.0.0.1:1/v1", "local-model")
|
||||
|
||||
code, stdout, stderr := runCLICommand(t, renderCommand, []string{
|
||||
"--prompt-dir", lib.promptDir,
|
||||
"--profile-dir", lib.profileDir,
|
||||
"--prompt", "plain",
|
||||
})
|
||||
if code != ExitOK {
|
||||
t.Fatalf("expected ExitOK, got %d stderr=%q", code, stderr)
|
||||
}
|
||||
if strings.Contains(stdout, "session_id:") {
|
||||
t.Fatalf("expected no effective session ID, got:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCommandRejectsOverlongSessionID(t *testing.T) {
|
||||
lib := newCLITestLibrary(t)
|
||||
writePromptDefinition(t, lib.promptDir, "session.yaml", `id: session
|
||||
version: "1"
|
||||
default_profile: local
|
||||
messages:
|
||||
- role: user
|
||||
content: "hello"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
`)
|
||||
writeProfileFile(t, lib.profileDir, "local", "http://127.0.0.1:1/v1", "local-model")
|
||||
|
||||
code, _, stderr := runCLICommand(t, renderCommand, []string{
|
||||
"--prompt-dir", lib.promptDir,
|
||||
"--profile-dir", lib.profileDir,
|
||||
"--prompt", "session",
|
||||
"--session-id", strings.Repeat("x", 257),
|
||||
})
|
||||
if code != ExitRuntimeError {
|
||||
t.Fatalf("expected runtime error, got %d stderr=%q", code, stderr)
|
||||
}
|
||||
if !strings.Contains(stderr, "invalid run request") {
|
||||
t.Fatalf("expected invalid-request context, got %q", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguredBackendValidationComesFromPromptkit(t *testing.T) {
|
||||
lib := newCLITestLibrary(t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user