Add structured output repair support

This commit is contained in:
2026-08-25 19:35:27 +00:00
parent 3bd3c7ebf7
commit ab9b743df6
4 changed files with 37 additions and 8 deletions

View File

@@ -344,7 +344,7 @@ git diff --check
- Capacity and context classifications remain unchanged and more specific.
- Security tests prove selected credentials and bearer tokens are not leaked.
## Stage 5: Add Adapter-Level Structured Repair Support
## Stage 5: Add Adapter-Level Structured Repair Support
### Goal

View File

@@ -9,13 +9,14 @@ import (
)
type StructuredCompletionRequest struct {
StageName string `json:"stage_name"`
PromptID string `json:"prompt_id,omitempty"`
PromptVersion string `json:"prompt_version,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
Inputs LLMInputSet `json:"inputs,omitempty"`
Vars map[string]any `json:"vars,omitempty"`
StageName string `json:"stage_name"`
PromptID string `json:"prompt_id,omitempty"`
PromptVersion string `json:"prompt_version,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
Inputs LLMInputSet `json:"inputs,omitempty"`
Vars map[string]any `json:"vars,omitempty"`
StructuredOutputRepairAttempts *int `json:"structured_output_repair_attempts,omitempty"`
}
type StructuredCompletionResponse struct {
@@ -26,6 +27,7 @@ type StructuredCompletionResponse struct {
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
RepairAttempts int `json:"repair_attempts,omitempty"`
Debug *LLMDebugMaterial `json:"debug,omitempty"`
}

View File

@@ -116,6 +116,9 @@ func (c *PromptKitClient) CompleteStructured(ctx context.Context, req contracts.
if err := validateOutputTarget(out); err != nil {
return contracts.StructuredCompletionResponse{}, err
}
if req.StructuredOutputRepairAttempts != nil && (*req.StructuredOutputRepairAttempts < 0 || *req.StructuredOutputRepairAttempts > 3) {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("structured output repair attempts must be between zero and three")
}
promptID := strings.TrimSpace(req.PromptID)
if promptID == "" {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("structured completion prompt_id must not be empty")
@@ -138,6 +141,15 @@ func (c *PromptKitClient) CompleteStructured(ctx context.Context, req contracts.
Vars: promptKitVars(req, sessionID),
Execution: execution,
}
if req.StructuredOutputRepairAttempts != nil {
inspection, err := c.engine.InspectPrompt(ctx, promptID, strings.TrimSpace(req.PromptVersion))
if err != nil {
return contracts.StructuredCompletionResponse{}, fmt.Errorf("inspect PromptKit prompt %q: %v", promptID, redactPromptKitError(err))
}
contract := inspection.OutputContract
contract.RepairAttempts = *req.StructuredOutputRepairAttempts
runReq.Validation = &contract
}
prepared, err := c.engine.PrepareExecution(ctx, runReq)
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
@@ -242,6 +254,7 @@ func (c *PromptKitClient) responseFromResult(result *promptkit.RunResult, prepar
PromptTokens: result.Usage.PromptTokens,
CompletionTokens: result.Usage.CompletionTokens,
TotalTokens: result.Usage.TotalTokens,
RepairAttempts: result.Validation.RepairAttempts,
Debug: promptKitDebugMaterial(prepared, result),
}
}

View File

@@ -667,6 +667,20 @@ func TestPromptKitClientMapsHTTPGenerationErrorToApplicationBoundary(t *testing.
}
}
func TestPromptKitClientRejectsInvalidRepairAttemptOverride(t *testing.T) {
for _, attempts := range []int{-1, 4} {
t.Run("invalid", func(t *testing.T) {
fake := &fakePromptKitLLM{content: `{"ok":true}`}
client := newTestPromptKitClient(t, fake)
var out map[string]any
_, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{PromptID: "adapter.test", StructuredOutputRepairAttempts: &attempts}, &out)
if err == nil || fake.calls != 0 {
t.Fatalf("error=%v calls=%d", err, fake.calls)
}
})
}
}
func TestPromptKitClientCheckpointFingerprintTracksFallbackProfileAssets(t *testing.T) {
fingerprintFor := func(content string) CheckpointFingerprint {
t.Helper()