Add session and reasoning controls

This commit is contained in:
2026-08-29 14:22:45 +00:00
parent 1a0f15e210
commit 06b37c2bae
8 changed files with 355 additions and 28 deletions

View File

@@ -8,6 +8,7 @@ type runRequestDTO struct {
PromptID string `json:"prompt_id"`
PromptVersion string `json:"prompt_version,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
Inputs map[string]inputRefDTO `json:"inputs"`
Vars map[string]string `json:"vars,omitempty"`
Model *modelOverrideRequestDTO `json:"model,omitempty"`
@@ -56,6 +57,7 @@ type metadataDTO struct {
PromptHash string `json:"prompt_hash"`
RenderedPromptHash string `json:"rendered_prompt_hash"`
SelectedProfileID string `json:"selected_profile_id"`
SessionID string `json:"session_id,omitempty"`
ModelName string `json:"model_name"`
Endpoint string `json:"endpoint"`
ModelParams modelParamsDTO `json:"model_params"`

View File

@@ -97,6 +97,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
PromptID: req.PromptID,
PromptVersion: req.PromptVersion,
ProfileID: req.ProfileID,
SessionID: req.SessionID,
Inputs: mappedInputs,
Vars: req.Vars,
Execution: model,
@@ -124,6 +125,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
PromptHash: res.PromptHash,
RenderedPromptHash: res.RenderedPromptHash,
SelectedProfileID: res.SelectedProfileID,
SessionID: res.SessionID,
ModelName: res.ModelName,
Endpoint: res.Endpoint,
ModelParams: modelParamsDTOFromExecutionTarget(res.EffectiveModelParams),

View File

@@ -514,7 +514,9 @@ func TestHandlerModelOverridePreservesReasoningEffortPresence(t *testing.T) {
wantValue string
}{
{name: "omitted", model: `{}`},
{name: "nonblank", model: `{"reasoning_effort":"high"}`, wantPresent: true, wantValue: "high"},
{name: "explicit empty", model: `{"reasoning_effort":""}`, wantPresent: true},
{name: "null", model: `{"reasoning_effort":null}`},
}
for _, tc := range tests {
@@ -546,6 +548,136 @@ func TestHandlerModelOverridePreservesReasoningEffortPresence(t *testing.T) {
}
}
func TestHandlerMapsSessionIDAndReportsEffectiveResultSessionID(t *testing.T) {
r := &fakeRunner{result: &promptkit.RunResult{
Artifact: promptkit.Artifact{Body: []byte("ok")},
Validation: promptkit.ValidationResult{Status: promptkit.ValidationPassed, Mode: promptkit.ValidationBasic, IsValid: true},
EffectiveModelParams: promptkit.ExecutionTarget{Endpoint: "http://llm/v1", Model: "m1"},
SessionID: "effective-session",
}}
h := NewHandler(r)
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"prompt_id":"prompt-1","session_id":"request-session"}`))
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
if r.last.SessionID != "request-session" {
t.Fatalf("expected session ID in run request, got %q", r.last.SessionID)
}
var resp map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON response: %v", err)
}
metadata := resp["metadata"].(map[string]any)
if metadata["session_id"] != "effective-session" {
t.Fatalf("expected effective session ID in response, got %#v", metadata["session_id"])
}
}
func TestHandlerOmitsEmptyEffectiveSessionID(t *testing.T) {
r := &fakeRunner{result: &promptkit.RunResult{
Artifact: promptkit.Artifact{Body: []byte("ok")},
Validation: promptkit.ValidationResult{Status: promptkit.ValidationPassed, Mode: promptkit.ValidationBasic, IsValid: true},
EffectiveModelParams: promptkit.ExecutionTarget{Endpoint: "http://llm/v1", Model: "m1"},
}}
h := NewHandler(r)
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"prompt_id":"prompt-1"}`))
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
var resp map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON response: %v", err)
}
metadata := resp["metadata"].(map[string]any)
if _, ok := metadata["session_id"]; ok {
t.Fatalf("expected empty effective session ID to be omitted, got %#v", metadata)
}
}
func TestHandlerSessionIDUsesPromptkitResolution(t *testing.T) {
for _, tc := range []struct {
name string
definitionSession string
requestSession string
wantSession string
}{
{name: "definition session", definitionSession: "definition-session", wantSession: "definition-session"},
{name: "direct session", definitionSession: "definition-session", requestSession: "direct-session", wantSession: "direct-session"},
{name: "no effective session"},
} {
t.Run(tc.name, func(t *testing.T) {
promptDir := t.TempDir()
profileDir := t.TempDir()
sessionLine := ""
if tc.definitionSession != "" {
sessionLine = "session_id: " + tc.definitionSession + "\n"
}
if err := os.WriteFile(filepath.Join(promptDir, "prompt.yaml"), []byte("id: p\nversion: \"1\"\ndefault_profile: exec\n"+sessionLine+`messages:
- role: user
content: "hi"
output:
format: text
validation_mode: none
`), 0o644); err != nil {
t.Fatalf("write prompt fixture: %v", err)
}
if err := os.WriteFile(filepath.Join(profileDir, "profile.yaml"), []byte("id: exec\nendpoint: http://127.0.0.1:1/v1\nmodel: test\n"), 0o644); err != nil {
t.Fatalf("write profile fixture: %v", err)
}
engine, err := promptkit.NewEngine(promptkit.Config{PromptDir: promptDir, ProfileDir: profileDir}, promptkit.WithLLMClient(handlerLLMClient{}))
if err != nil {
t.Fatalf("new engine: %v", err)
}
body := `{"prompt_id":"p"}`
if tc.requestSession != "" {
body = `{"prompt_id":"p","session_id":"` + tc.requestSession + `"}`
}
w := httptest.NewRecorder()
NewHandler(engine).ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(body)))
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
var resp map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON response: %v", err)
}
metadata := resp["metadata"].(map[string]any)
if tc.wantSession == "" {
if _, ok := metadata["session_id"]; ok {
t.Fatalf("expected session_id to be omitted, got %#v", metadata)
}
return
}
if metadata["session_id"] != tc.wantSession {
t.Fatalf("expected effective session ID %q, got %#v", tc.wantSession, metadata["session_id"])
}
})
}
}
func TestHandlerRejectsOverlongSessionIDAndNonStringReasoningEffort(t *testing.T) {
engine := newHandlerEngine(t)
for _, body := range []string{
`{"prompt_id":"p","session_id":"` + strings.Repeat("x", 257) + `"}`,
`{"prompt_id":"p","model":{"reasoning_effort":1}}`,
} {
w := httptest.NewRecorder()
NewHandler(engine).ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(body)))
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d body=%s", w.Code, w.Body.String())
}
}
}
func TestHandlerModelOverrideAcceptsJSONCompatibleExtraParams(t *testing.T) {
r := &fakeRunner{result: &promptkit.RunResult{
Artifact: promptkit.Artifact{Body: []byte("ok")},