Refactor: split prompt definition from execution settings and migrate run contracts to prompt_* + execution_target

This commit is contained in:
2026-05-05 10:09:31 -05:00
parent fdfd8641f5
commit a633c67538
28 changed files with 712 additions and 1021 deletions

View File

@@ -5,11 +5,12 @@ import (
)
type runRequestDTO struct {
ProfileID string `json:"profile_id"`
ProfileVersion string `json:"profile_version,omitempty"`
Inputs map[string]inputRefDTO `json:"inputs"`
Vars map[string]string `json:"vars,omitempty"`
Model *modelOverrideRequestDTO `json:"model,omitempty"`
PromptID string `json:"prompt_id"`
PromptVersion string `json:"prompt_version,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
Inputs map[string]inputRefDTO `json:"inputs"`
Vars map[string]string `json:"vars,omitempty"`
Model *modelOverrideRequestDTO `json:"model,omitempty"`
}
type inputRefDTO struct {
@@ -19,12 +20,15 @@ type inputRefDTO struct {
}
type modelOverrideRequestDTO struct {
Endpoint string `json:"endpoint,omitempty"`
Model string `json:"model,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
TopP float64 `json:"top_p,omitempty"`
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Model string `json:"model,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
TopP float64 `json:"top_p,omitempty"`
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
ReasoningEffort string `json:"reasoning_effort,omitempty"`
APIKeyEnv string `json:"api_key_env,omitempty"`
ExtraParams map[string]string `json:"extra_params,omitempty"`
}
type runResponseDTO struct {
@@ -45,14 +49,15 @@ type artifactDTO struct {
type metadataDTO struct {
RunID string `json:"run_id"`
ProfileID string `json:"profile_id"`
ProfileVersion string `json:"profile_version"`
ProfileHash string `json:"profile_hash"`
PromptID string `json:"prompt_id"`
PromptVersion string `json:"prompt_version"`
PromptHash string `json:"prompt_hash"`
RenderedPromptHash string `json:"rendered_prompt_hash"`
SelectedProfileID string `json:"selected_profile_id"`
ModelName string `json:"model_name"`
Endpoint string `json:"endpoint"`
ModelParams modelParamsDTO `json:"model_params"`
InputHashes map[string]string `json:"input_hashes"`
PromptHash string `json:"prompt_hash"`
Usage tokenUsageDTO `json:"usage"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
@@ -63,12 +68,15 @@ type metadataDTO struct {
}
type modelParamsDTO struct {
Endpoint string `json:"endpoint"`
Model string `json:"model"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
TopP float64 `json:"top_p"`
TimeoutSeconds int `json:"timeout_seconds"`
Endpoint string `json:"endpoint"`
Model string `json:"model"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
TopP float64 `json:"top_p"`
TimeoutSeconds int `json:"timeout_seconds"`
ReasoningEffort string `json:"reasoning_effort,omitempty"`
APIKeyEnv string `json:"api_key_env,omitempty"`
ExtraParams map[string]string `json:"extra_params,omitempty"`
}
type tokenUsageDTO struct {

View File

@@ -40,8 +40,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if strings.TrimSpace(req.ProfileID) == "" {
writeError(w, http.StatusBadRequest, "invalid_request", "profile_id is required")
if strings.TrimSpace(req.PromptID) == "" {
writeError(w, http.StatusBadRequest, "invalid_request", "prompt_id is required")
return
}
if len(req.Inputs) == 0 {
@@ -58,24 +58,28 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
var model *domain.ModelTarget
var model *domain.ExecutionTarget
if req.Model != nil {
model = &domain.ModelTarget{
Endpoint: req.Model.Endpoint,
Model: req.Model.Model,
Temperature: req.Model.Temperature,
MaxTokens: req.Model.MaxTokens,
TopP: req.Model.TopP,
TimeoutSeconds: req.Model.TimeoutSeconds,
model = &domain.ExecutionTarget{
Endpoint: req.Model.Endpoint,
Model: req.Model.Model,
Temperature: req.Model.Temperature,
MaxTokens: req.Model.MaxTokens,
TopP: req.Model.TopP,
TimeoutSeconds: req.Model.TimeoutSeconds,
ReasoningEffort: req.Model.ReasoningEffort,
APIKeyEnv: req.Model.APIKeyEnv,
ExtraParams: req.Model.ExtraParams,
}
}
res, err := h.runner.Run(r.Context(), domain.RunRequest{
ProfileID: req.ProfileID,
ProfileVersion: req.ProfileVersion,
Inputs: mappedInputs,
Vars: req.Vars,
Model: model,
PromptID: req.PromptID,
PromptVersion: req.PromptVersion,
ProfileID: req.ProfileID,
Inputs: mappedInputs,
Vars: req.Vars,
Execution: model,
})
if err != nil {
status, code, message := mapRunError(err)
@@ -94,22 +98,26 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
},
Validation: mapValidation(res.Validation),
Metadata: metadataDTO{
RunID: res.RunID,
ProfileID: res.ProfileID,
ProfileVersion: res.ProfileVersion,
ProfileHash: res.ProfileHash,
ModelName: res.ModelName,
Endpoint: res.Endpoint,
RunID: res.RunID,
PromptID: res.PromptID,
PromptVersion: res.PromptVersion,
PromptHash: res.PromptHash,
RenderedPromptHash: res.RenderedPromptHash,
SelectedProfileID: res.SelectedProfileID,
ModelName: res.ModelName,
Endpoint: res.Endpoint,
ModelParams: modelParamsDTO{
Endpoint: res.ModelParams.Endpoint,
Model: res.ModelParams.Model,
Temperature: res.ModelParams.Temperature,
MaxTokens: res.ModelParams.MaxTokens,
TopP: res.ModelParams.TopP,
TimeoutSeconds: res.ModelParams.TimeoutSeconds,
Endpoint: res.EffectiveModelParams.Endpoint,
Model: res.EffectiveModelParams.Model,
Temperature: res.EffectiveModelParams.Temperature,
MaxTokens: res.EffectiveModelParams.MaxTokens,
TopP: res.EffectiveModelParams.TopP,
TimeoutSeconds: res.EffectiveModelParams.TimeoutSeconds,
ReasoningEffort: res.EffectiveModelParams.ReasoningEffort,
APIKeyEnv: res.EffectiveModelParams.APIKeyEnv,
ExtraParams: res.EffectiveModelParams.ExtraParams,
},
InputHashes: res.InputHashes,
PromptHash: res.PromptHash,
Usage: tokenUsageDTO{
PromptTokens: res.Usage.PromptTokens,
CompletionTokens: res.Usage.CompletionTokens,
@@ -140,11 +148,11 @@ func mapValidation(v domain.ValidationResult) validationDTO {
func mapRunError(err error) (int, string, string) {
switch {
case errors.Is(err, profile.ErrProfileNotFound):
return http.StatusNotFound, "profile_not_found", "profile not found"
return http.StatusNotFound, "prompt_not_found", "prompt definition not found"
case errors.Is(err, usecase.ErrInvalidRequest):
return http.StatusBadRequest, "invalid_request", "invalid run request"
case errors.Is(err, usecase.ErrProfileLoad):
return http.StatusBadRequest, "profile_load_failed", "failed to load profile"
return http.StatusBadRequest, "prompt_load_failed", "failed to load prompt definition"
case errors.Is(err, usecase.ErrArtifactLoad):
return http.StatusBadRequest, "artifact_read_failed", "failed to read input artifact"
case errors.Is(err, usecase.ErrPromptRender):

View File

@@ -42,13 +42,15 @@ func TestHandlerPostRunsSuccess(t *testing.T) {
Size: 5,
Hash: "abc",
},
Validation: domain.ValidationResult{Status: domain.ValidationPassed, Mode: domain.ValidationBasic, IsValid: true},
ProfileID: "p1",
ProfileVersion: "1.0.0",
ProfileHash: "phash",
ModelName: "m1",
Endpoint: "http://llm/v1",
ModelParams: domain.ModelTarget{
Validation: domain.ValidationResult{Status: domain.ValidationPassed, Mode: domain.ValidationBasic, IsValid: true},
PromptID: "prompt-1",
PromptVersion: "1.0.0",
PromptHash: "phash",
RenderedPromptHash: "rhash",
SelectedProfileID: "exec-default",
ModelName: "m1",
Endpoint: "http://llm/v1",
EffectiveModelParams: domain.ExecutionTarget{
Endpoint: "http://llm/v1",
Model: "m1",
Temperature: 0.2,
@@ -57,7 +59,6 @@ func TestHandlerPostRunsSuccess(t *testing.T) {
TimeoutSeconds: 120,
},
InputHashes: map[string]string{"transcript": "h1"},
PromptHash: "ph",
Usage: domain.TokenUsage{PromptTokens: 1, CompletionTokens: 2, TotalTokens: 3},
StartTime: start,
EndTime: end,
@@ -68,7 +69,8 @@ func TestHandlerPostRunsSuccess(t *testing.T) {
h := NewHandler(r)
body := []byte(`{
"profile_id": "p1",
"prompt_id": "prompt-1",
"profile_id": "exec-default",
"inputs": {
"transcript": {"type": "file", "uri": "./t.md"}
},
@@ -101,8 +103,8 @@ func TestHandlerPostRunsSuccess(t *testing.T) {
if metadata["run_id"] != "11111111-1111-4111-8111-111111111111" {
t.Fatalf("unexpected metadata.run_id: %#v", metadata["run_id"])
}
if metadata["profile_hash"] != "phash" {
t.Fatalf("unexpected metadata.profile_hash: %#v", metadata["profile_hash"])
if metadata["prompt_hash"] != "phash" {
t.Fatalf("unexpected metadata.prompt_hash: %#v", metadata["prompt_hash"])
}
usage := metadata["usage"].(map[string]any)
if usage["total_tokens"] != float64(3) {
@@ -122,14 +124,17 @@ func TestHandlerPostRunsSuccess(t *testing.T) {
t.Fatalf("expected raw model output hello, got %#v", resp["raw_model_output"])
}
if r.last.ProfileID != "p1" {
t.Fatalf("expected request profile_id p1, got %q", r.last.ProfileID)
if r.last.PromptID != "prompt-1" {
t.Fatalf("expected request prompt_id prompt-1, got %q", r.last.PromptID)
}
if r.last.Model == nil || r.last.Model.Model != "gpt-x" {
t.Fatalf("expected model override, got %#v", r.last.Model)
if r.last.ProfileID != "exec-default" {
t.Fatalf("expected request profile_id exec-default, got %q", r.last.ProfileID)
}
if r.last.Model.TimeoutSeconds != 120 {
t.Fatalf("expected timeout_seconds override 120, got %#v", r.last.Model)
if r.last.Execution == nil || r.last.Execution.Model != "gpt-x" {
t.Fatalf("expected model override, got %#v", r.last.Execution)
}
if r.last.Execution.TimeoutSeconds != 120 {
t.Fatalf("expected timeout_seconds override 120, got %#v", r.last.Execution)
}
}
@@ -145,7 +150,7 @@ func TestHandlerInvalidJSON(t *testing.T) {
}
}
func TestHandlerMissingProfileID(t *testing.T) {
func TestHandlerMissingPromptID(t *testing.T) {
h := NewHandler(&fakeRunner{})
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"inputs":{"x":{"type":"file","uri":"a"}}}`))
w := httptest.NewRecorder()
@@ -173,7 +178,7 @@ func TestHandlerUsecaseErrorMapping(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := NewHandler(&fakeRunner{err: tc.err})
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"profile_id":"p","inputs":{"x":{"type":"file","uri":"a"}}}`))
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"prompt_id":"p","inputs":{"x":{"type":"file","uri":"a"}}}`))
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
@@ -210,7 +215,7 @@ func TestHandlerValidationFailureStillSuccess(t *testing.T) {
},
}})
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"profile_id":"p","inputs":{"x":{"type":"file","uri":"a"}}}`))
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"prompt_id":"p","inputs":{"x":{"type":"file","uri":"a"}}}`))
w := httptest.NewRecorder()
h.ServeHTTP(w, req)