feat(ai): add PromptHash to ProviderError + log on schema violation
promptHashShort(system+"\x00"+user)[:12] computed on ErrSchemaViolation and attached to ProviderError.PromptHash. research.go schema-violation log now includes prompt_hash for cross-referencing ai_usage rows.
This commit is contained in:
@@ -80,7 +80,11 @@ func (h *ResearchHandler) Research(c *gin.Context) {
|
||||
c.JSON(http.StatusServiceUnavailable, apierror.NewResponse(apierror.BadRequest("rate_limited", "KI rate limit erreicht, bitte kurz warten")))
|
||||
return
|
||||
case ai.ErrSchemaViolation:
|
||||
slog.ErrorContext(ctx, "research schema violation", "market_id", id, "raw", pe.RawOutput, "inner", pe.Inner)
|
||||
slog.ErrorContext(ctx, "research schema violation",
|
||||
"market_id", id,
|
||||
"prompt_hash", pe.PromptHash,
|
||||
"raw", pe.RawOutput,
|
||||
"inner", pe.Inner)
|
||||
c.JSON(http.StatusInternalServerError, apierror.NewResponse(apierror.Internal("Modell hat ungültige Ausgabe geliefert")))
|
||||
return
|
||||
case ai.ErrInvalidRequest:
|
||||
|
||||
@@ -42,11 +42,12 @@ func (c ErrorCode) String() string {
|
||||
}
|
||||
|
||||
type ProviderError struct {
|
||||
Code ErrorCode
|
||||
Message string
|
||||
Retryable bool
|
||||
Inner error
|
||||
RawOutput string
|
||||
Code ErrorCode
|
||||
Message string
|
||||
Retryable bool
|
||||
Inner error
|
||||
RawOutput string
|
||||
PromptHash string // sha256(system+"\x00"+user)[:12], set on ErrSchemaViolation
|
||||
}
|
||||
|
||||
func (e *ProviderError) Error() string {
|
||||
|
||||
@@ -2,6 +2,8 @@ package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
@@ -258,12 +260,14 @@ func (p *GeminiProvider) Chat(ctx context.Context, req *ChatRequest) (*ChatRespo
|
||||
text := resp.Text()
|
||||
if len(req.JSONSchema) > 0 {
|
||||
if verr := ValidateSchema(req.JSONSchema, []byte(text)); verr != nil {
|
||||
promptHash := promptHashShort(req.SystemPrompt, req.UserMessage)
|
||||
return nil, &ProviderError{
|
||||
Code: ErrSchemaViolation,
|
||||
Message: fmt.Sprintf("response does not match schema: %v", verr),
|
||||
Retryable: true,
|
||||
Inner: verr,
|
||||
RawOutput: text,
|
||||
Code: ErrSchemaViolation,
|
||||
Message: fmt.Sprintf("response does not match schema: %v", verr),
|
||||
Retryable: true,
|
||||
Inner: verr,
|
||||
RawOutput: text,
|
||||
PromptHash: promptHash,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,6 +334,13 @@ func (p *GeminiProvider) record(ctx context.Context, e UsageEvent) {
|
||||
_ = p.recorder.Record(ctx, e)
|
||||
}
|
||||
|
||||
// promptHashShort returns the first 12 hex chars of sha256(system+"\x00"+user).
|
||||
// Used to correlate schema-violation errors with the prompt that produced them.
|
||||
func promptHashShort(system, user string) string {
|
||||
h := sha256.Sum256([]byte(system + "\x00" + user))
|
||||
return hex.EncodeToString(h[:])[:12]
|
||||
}
|
||||
|
||||
// schemaFromMap converts a raw JSON-schema map to genai.Schema for structured output.
|
||||
func schemaFromMap(m map[string]any) *genai.Schema {
|
||||
s := &genai.Schema{}
|
||||
|
||||
Reference in New Issue
Block a user