Use PromptKit prepared execution for structured model calls

This commit is contained in:
2026-09-24 01:57:25 +00:00
parent 96d3d03bee
commit c8a78bf024
17 changed files with 560 additions and 1366 deletions

View File

@@ -3,6 +3,7 @@ package structuredoutput
import (
"context"
"encoding/json"
"time"
"gitea.maximumdirect.net/eric/audita/internal/framework/responseschema"
)
@@ -39,13 +40,38 @@ type PromptMetadata struct {
}
type Response struct {
Content json.RawMessage `json:"content,omitempty"`
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
Prompt PromptMetadata `json:"prompt"`
Content json.RawMessage `json:"content,omitempty"`
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
Prompt PromptMetadata `json:"prompt"`
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
FinalContent string `json:"final_content,omitempty"`
}
// PreparedSnapshot is the exact rendered request snapshot. Its messages may
// contain transcript and glossary content and belong only in protected diagnostics.
type PreparedSnapshot struct {
Messages []Message `json:"messages"`
Prompt PromptMetadata `json:"prompt"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// CallRecord brackets one logical operation, including preparation. Content
// is present only when a model returned it; consumers must protect it.
type CallRecord struct {
Context DiagnosticContext `json:"context"`
Started time.Time `json:"started"`
Finished time.Time `json:"finished,omitempty"`
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
FinalContent string `json:"final_content,omitempty"`
Failure FailureKind `json:"failure,omitempty"`
}
type Client interface {

View File

@@ -0,0 +1,27 @@
package structuredoutput
import "errors"
// FailureKind separates invalid generated content from operational failures.
type FailureKind string
const (
FailureMalformedOutput FailureKind = "malformed_output"
FailureRuntime FailureKind = "runtime"
)
// Failure exposes a safe message while retaining the typed cause for callers.
// The cause must not be serialized or formatted directly in user artifacts.
type Failure struct {
Kind FailureKind
Message string
Cause error
}
func (f *Failure) Error() string { return f.Message }
func (f *Failure) Unwrap() error { return f.Cause }
func IsMalformedError(err error) bool {
var failure *Failure
return errors.As(err, &failure) && failure.Kind == FailureMalformedOutput
}

View File

@@ -0,0 +1,18 @@
package structuredoutput
import (
"errors"
"testing"
)
func TestFailureClassificationKeepsCauseAndSafeMessage(t *testing.T) {
cause := errors.New("provider token secret-value")
malformed := &Failure{Kind: FailureMalformedOutput, Message: "malformed structured output", Cause: cause}
if !IsMalformedError(malformed) || !errors.Is(malformed, cause) || malformed.Error() != "malformed structured output" {
t.Fatalf("malformed classification: %v", malformed)
}
runtime := &Failure{Kind: FailureRuntime, Message: "structured model call failed", Cause: cause}
if IsMalformedError(runtime) || IsMalformedError(cause) || IsMalformedError(nil) || !errors.Is(runtime, cause) || runtime.Error() != "structured model call failed" {
t.Fatalf("runtime classification: %v", runtime)
}
}

View File

@@ -1,28 +0,0 @@
package structuredoutput
import "strings"
var malformedMarkers = []string{
"malformed structured output",
"decode structured output:",
"decode provider response envelope:",
"provider response missing choices",
"provider response missing assistant message content",
"provider response assistant message content is empty",
"provider response assistant message content is not valid JSON",
}
// IsMalformedError reports whether err matches provider malformed
// structured-output failure markers that should be downgraded.
func IsMalformedError(err error) bool {
if err == nil {
return false
}
msg := err.Error()
for _, marker := range malformedMarkers {
if strings.Contains(msg, marker) {
return true
}
}
return false
}

View File

@@ -1,32 +0,0 @@
package structuredoutput
import (
"errors"
"testing"
)
func TestIsMalformedError(t *testing.T) {
cases := []struct {
name string
err error
want bool
}{
{name: "nil", err: nil, want: false},
{name: "generic", err: errors.New("network timeout"), want: false},
{name: "malformed", err: errors.New("malformed structured output"), want: true},
{name: "decode structured", err: errors.New("decode structured output: unexpected end of JSON input"), want: true},
{name: "missing choices", err: errors.New("provider response missing choices"), want: true},
{name: "missing content", err: errors.New("provider response missing assistant message content"), want: true},
{name: "empty content", err: errors.New("provider response assistant message content is empty"), want: true},
{name: "invalid content json", err: errors.New("provider response assistant message content is not valid JSON"), want: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := IsMalformedError(tc.err)
if got != tc.want {
t.Fatalf("IsMalformedError(%v): got=%v want=%v", tc.err, got, tc.want)
}
})
}
}