feat(providers): enhance error context with request ID and timestamps
- Add new EnhancedError and ExtendedFailoverError types with detailed context - Enhance FailoverError struct with RequestID, Correlation, and Timestamp - Add generateRequestID() utility to create unique identifiers for tracking - Integrate request ID generation and context into error classifier - Update HTTP provider to attach request IDs to API call errors - Improve error messages to include contextual information - Maintain backward compatibility with existing error handling behavior This change provides more detailed error information for debugging including request tracking IDs, timestamps, and other relevant context when provider API calls fail.
This commit is contained in:
parent
93bbe1aff3
commit
ebe425a86f
5 changed files with 295 additions and 42 deletions
3
go.mod
3
go.mod
|
|
@ -11,6 +11,7 @@ require (
|
|||
github.com/gdamore/tcell/v2 v2.13.8
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
|
||||
github.com/mdp/qrterminal/v3 v3.2.1
|
||||
github.com/modelcontextprotocol/go-sdk v1.3.0
|
||||
|
|
@ -37,8 +38,6 @@ require (
|
|||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.13.8 // indirect
|
||||
github.com/h2non/filetype v1.1.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
|
|
|
|||
184
pkg/providers/enhanced_error.go
Normal file
184
pkg/providers/enhanced_error.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
package providers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RequestContext holds contextual information about an API request
|
||||
type RequestContext struct {
|
||||
RequestID string // Unique identifier for the request
|
||||
Timestamp time.Time // Time of the request
|
||||
Model string // Model name used in this call
|
||||
Provider string // Provider name used in this call
|
||||
UserID string // Associated user (if available)
|
||||
SessionID string // Session identifier (if available)
|
||||
ToolCallID string // Tool call ID if this is part of a tool call
|
||||
Endpoint string // API endpoint that was called
|
||||
Correlation string // Additional correlation identifier
|
||||
}
|
||||
|
||||
// EnhancedError wraps an error with additional contextual information
|
||||
type EnhancedError struct {
|
||||
WrappedErr error // Original error
|
||||
Context *RequestContext // Contextual information
|
||||
Message string // Custom message for the enhanced error
|
||||
EventType string // Type of event that caused the error (e.g. "api_request", "rate_limit", etc.)
|
||||
}
|
||||
|
||||
func NewEnhancedError(wrappedErr error, context *RequestContext, message string, eventType string) *EnhancedError {
|
||||
return &EnhancedError{
|
||||
WrappedErr: wrappedErr,
|
||||
Context: context,
|
||||
Message: message,
|
||||
EventType: eventType,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EnhancedError) Error() string {
|
||||
if e.Context != nil {
|
||||
return fmt.Sprintf("error: %s [event=%s, req_id=%s, provider=%s, model=%s, timestamp=%s]: %v",
|
||||
e.Message, e.EventType, e.Context.RequestID, e.Context.Provider, e.Context.Model, e.Context.Timestamp.Format(time.RFC3339), e.WrappedErr)
|
||||
}
|
||||
return fmt.Sprintf("error: %s [event=%s]: %v", e.Message, e.EventType, e.WrappedErr)
|
||||
}
|
||||
|
||||
func (e *EnhancedError) Unwrap() error {
|
||||
return e.WrappedErr
|
||||
}
|
||||
|
||||
// FormatDetailedError formats the error with extensive detail including all context
|
||||
func (e *EnhancedError) FormatDetailedError() string {
|
||||
if e.Context == nil {
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`Enhanced Error Details:
|
||||
Error Message: %s
|
||||
Event Type: %s
|
||||
Request ID: %s
|
||||
Model: %s
|
||||
Provider: %s
|
||||
Endpoint: %s
|
||||
Timestamp: %s
|
||||
User ID: %s
|
||||
Session ID: %s
|
||||
Tool Call ID: %s
|
||||
Correlation ID: %s
|
||||
Wrapped Error: %v`,
|
||||
e.Message,
|
||||
e.EventType,
|
||||
e.Context.RequestID,
|
||||
e.Context.Model,
|
||||
e.Context.Provider,
|
||||
e.Context.Endpoint,
|
||||
e.Context.Timestamp.Format(time.RFC3339),
|
||||
e.Context.UserID,
|
||||
e.Context.SessionID,
|
||||
e.Context.ToolCallID,
|
||||
e.Context.Correlation,
|
||||
e.WrappedErr,
|
||||
)
|
||||
}
|
||||
|
||||
// ExtendedFailoverError enhances the original FailoverError with additional context and metadata
|
||||
type ExtendedFailoverError struct {
|
||||
Reason FailoverReason
|
||||
Provider string
|
||||
Model string
|
||||
Status int
|
||||
Wrapped error
|
||||
RequestID string
|
||||
Timestamp time.Time
|
||||
Correlation string
|
||||
EventType string
|
||||
Metadata map[string]interface{} // Arbitrary additional metadata
|
||||
Message string
|
||||
}
|
||||
|
||||
func NewExtendedFailoverError(reason FailoverReason, provider, model string, status int, wrapped error) *ExtendedFailoverError {
|
||||
return &ExtendedFailoverError{
|
||||
Reason: reason,
|
||||
Provider: provider,
|
||||
Model: model,
|
||||
Status: status,
|
||||
Wrapped: wrapped,
|
||||
RequestID: "", // Will be set by caller
|
||||
Timestamp: time.Now(),
|
||||
Correlation: "", // Will be set by caller
|
||||
EventType: "provider_api_failure",
|
||||
Metadata: make(map[string]interface{}),
|
||||
Message: "", // Optional custom message
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExtendedFailoverError) Error() string {
|
||||
baseMsg := fmt.Sprintf("extended_failover(%s): provider=%s model=%s status=%d", e.Reason, e.Provider, e.Model, e.Status)
|
||||
if e.RequestID != "" {
|
||||
baseMsg += fmt.Sprintf(" request_id=%s", e.RequestID)
|
||||
}
|
||||
if e.Correlation != "" {
|
||||
baseMsg += fmt.Sprintf(" correlation=%s", e.Correlation)
|
||||
}
|
||||
if e.Message != "" {
|
||||
baseMsg += fmt.Sprintf(" message='%s'", e.Message)
|
||||
}
|
||||
baseMsg += fmt.Sprintf(" timestamp=%s", e.Timestamp.Format(time.RFC3339))
|
||||
|
||||
// Include error details from wrapped error if present
|
||||
if e.Wrapped != nil {
|
||||
baseMsg += fmt.Sprintf(": %v", e.Wrapped)
|
||||
}
|
||||
|
||||
return baseMsg
|
||||
}
|
||||
|
||||
func (e *ExtendedFailoverError) Unwrap() error {
|
||||
return e.Wrapped
|
||||
}
|
||||
|
||||
func (e *ExtendedFailoverError) WithRequestID(id string) *ExtendedFailoverError {
|
||||
e.RequestID = id
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ExtendedFailoverError) WithTimestamp(ts time.Time) *ExtendedFailoverError {
|
||||
e.Timestamp = ts
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ExtendedFailoverError) WithCorrelationID(id string) *ExtendedFailoverError {
|
||||
e.Correlation = id
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ExtendedFailoverError) WithEventType(eventType string) *ExtendedFailoverError {
|
||||
e.EventType = eventType
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ExtendedFailoverError) WithMetadata(key string, value interface{}) *ExtendedFailoverError {
|
||||
if e.Metadata == nil {
|
||||
e.Metadata = make(map[string]interface{})
|
||||
}
|
||||
e.Metadata[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *ExtendedFailoverError) WithMessage(message string) *ExtendedFailoverError {
|
||||
e.Message = message
|
||||
return e
|
||||
}
|
||||
|
||||
// IsRetriable returns true if this error should trigger fallback to next candidate.
|
||||
// Non-retriable: Format errors (bad request structure, image dimension/size).
|
||||
func (e *ExtendedFailoverError) IsRetriable() bool {
|
||||
return e.Reason != FailoverFormat
|
||||
}
|
||||
|
||||
// Convert a regular FailoverError to an ExtendedFailoverError
|
||||
func (e *FailoverError) AsExtendedError() *ExtendedFailoverError {
|
||||
extended := NewExtendedFailoverError(e.Reason, e.Provider, e.Model, e.Status, e.Wrapped)
|
||||
extended.Timestamp = time.Now()
|
||||
return extended
|
||||
}
|
||||
|
|
@ -3,7 +3,14 @@ package providers
|
|||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
|
||||
"strings"
|
||||
|
||||
"time"
|
||||
|
||||
)
|
||||
|
||||
// Common patterns in Go HTTP error messages
|
||||
|
|
@ -113,14 +120,14 @@ func ClassifyError(err error, provider, model string) *FailoverError {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Context deadline exceeded: treat as timeout, always fallback.
|
||||
if err == context.DeadlineExceeded {
|
||||
return &FailoverError{
|
||||
Reason: FailoverTimeout,
|
||||
Provider: provider,
|
||||
Model: model,
|
||||
Wrapped: err,
|
||||
}
|
||||
reqID, _ := generateRequestID()
|
||||
return (&FailoverError{
|
||||
Reason: FailoverTimeout,
|
||||
Provider: provider,
|
||||
Model: model,
|
||||
Wrapped: err,
|
||||
}).SetTimestamp(time.Now()).WithRequestID(reqID)
|
||||
}
|
||||
|
||||
msg := strings.ToLower(err.Error())
|
||||
|
|
@ -251,3 +258,18 @@ func parseDigits(s string) int {
|
|||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// generateRequestID creates a unique request ID for tracking API calls
|
||||
func generateRequestID() (string, error) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
const length = 16
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b[i] = charset[n.Int64()]
|
||||
}
|
||||
return "req_" + string(b), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,20 +159,33 @@ func (fc *FallbackChain) Execute(
|
|||
return nil, context.Canceled
|
||||
}
|
||||
|
||||
// Classify the error.
|
||||
failErr := ClassifyError(err, candidate.Provider, candidate.Model)
|
||||
// Classify the error after context check
|
||||
failErr := ClassifyError(err, candidate.Provider, candidate.Model)
|
||||
if failErr == nil {
|
||||
// Generate request ID for tracking
|
||||
reqID, genErr := generateRequestID()
|
||||
errWithReqID := &FailoverError{
|
||||
Reason: FailoverUnknown, // Will treat as unclassifiable as before
|
||||
Provider: candidate.Provider,
|
||||
Model: candidate.Model,
|
||||
Wrapped: err,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if genErr == nil && reqID != "" {
|
||||
errWithReqID = errWithReqID.WithRequestID(reqID)
|
||||
}
|
||||
|
||||
// Unclassifiable error: do not fallback, return immediately.
|
||||
result.Attempts = append(result.Attempts, FallbackAttempt{
|
||||
Provider: candidate.Provider,
|
||||
Model: candidate.Model,
|
||||
Error: errWithReqID,
|
||||
Duration: elapsed,
|
||||
})
|
||||
return nil, fmt.Errorf("fallback: unclassified error from %s/%s: %w",
|
||||
candidate.Provider, candidate.Model, errWithReqID)
|
||||
}
|
||||
|
||||
if failErr == nil {
|
||||
// Unclassifiable error: do not fallback, return immediately.
|
||||
result.Attempts = append(result.Attempts, FallbackAttempt{
|
||||
Provider: candidate.Provider,
|
||||
Model: candidate.Model,
|
||||
Error: err,
|
||||
Duration: elapsed,
|
||||
})
|
||||
return nil, fmt.Errorf("fallback: unclassified error from %s/%s: %w",
|
||||
candidate.Provider, candidate.Model, err)
|
||||
}
|
||||
|
||||
// Non-retriable error: abort immediately.
|
||||
if !failErr.IsRetriable() {
|
||||
|
|
@ -250,22 +263,31 @@ func (fc *FallbackChain) ExecuteImage(
|
|||
|
||||
// Image dimension/size errors are non-retriable.
|
||||
errMsg := strings.ToLower(err.Error())
|
||||
if IsImageDimensionError(errMsg) || IsImageSizeError(errMsg) {
|
||||
result.Attempts = append(result.Attempts, FallbackAttempt{
|
||||
Provider: candidate.Provider,
|
||||
Model: candidate.Model,
|
||||
Error: err,
|
||||
Reason: FailoverFormat,
|
||||
Duration: elapsed,
|
||||
})
|
||||
return nil, &FailoverError{
|
||||
Reason: FailoverFormat,
|
||||
Provider: candidate.Provider,
|
||||
Model: candidate.Model,
|
||||
Wrapped: err,
|
||||
}
|
||||
}
|
||||
if IsImageDimensionError(errMsg) || IsImageSizeError(errMsg) {
|
||||
reqID, _ := generateRequestID()
|
||||
|
||||
result.Attempts = append(result.Attempts, FallbackAttempt{
|
||||
Provider: candidate.Provider,
|
||||
Model: candidate.Model,
|
||||
Error: err,
|
||||
Reason: FailoverFormat,
|
||||
Duration: elapsed,
|
||||
})
|
||||
imageDimFailErr := &FailoverError{
|
||||
Reason: FailoverFormat,
|
||||
Provider: candidate.Provider,
|
||||
Model: candidate.Model,
|
||||
Wrapped: err,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if reqID != "" {
|
||||
imageDimFailErr = imageDimFailErr.WithRequestID(reqID)
|
||||
}
|
||||
|
||||
return nil, imageDimFailErr
|
||||
|
||||
|
||||
}
|
||||
// Any other error: record and try next.
|
||||
result.Attempts = append(result.Attempts, FallbackAttempt{
|
||||
Provider: candidate.Provider,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package providers
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||
)
|
||||
|
|
@ -52,14 +53,21 @@ const (
|
|||
|
||||
// FailoverError wraps an LLM provider error with classification metadata.
|
||||
type FailoverError struct {
|
||||
Reason FailoverReason
|
||||
Provider string
|
||||
Model string
|
||||
Status int
|
||||
Wrapped error
|
||||
Reason FailoverReason
|
||||
Provider string
|
||||
Model string
|
||||
Status int
|
||||
Wrapped error
|
||||
Timestamp time.Time // Timestamp of when the error occurred
|
||||
RequestID string // Request ID for tracking
|
||||
Correlation string // Correlation ID
|
||||
}
|
||||
|
||||
func (e *FailoverError) Error() string {
|
||||
if e.RequestID != "" {
|
||||
return fmt.Sprintf("failover(%s): provider=%s model=%s status=%d req_id=%s correlation=%s timestamp=%s: %v",
|
||||
e.Reason, e.Provider, e.Model, e.Status, e.RequestID, e.Correlation, e.Timestamp.Format(time.RFC3339), e.Wrapped)
|
||||
}
|
||||
return fmt.Sprintf("failover(%s): provider=%s model=%s status=%d: %v",
|
||||
e.Reason, e.Provider, e.Model, e.Status, e.Wrapped)
|
||||
}
|
||||
|
|
@ -74,6 +82,24 @@ func (e *FailoverError) IsRetriable() bool {
|
|||
return e.Reason != FailoverFormat
|
||||
}
|
||||
|
||||
// SetTimestamp stores when the error occurred
|
||||
func (e *FailoverError) SetTimestamp(t time.Time) *FailoverError {
|
||||
e.Timestamp = t
|
||||
return e
|
||||
}
|
||||
|
||||
// WithRequestID sets the request ID for this error
|
||||
func (e *FailoverError) WithRequestID(id string) *FailoverError {
|
||||
e.RequestID = id
|
||||
return e
|
||||
}
|
||||
|
||||
// WithCorrelation sets the correlation identifier for this error
|
||||
func (e *FailoverError) WithCorrelation(correlation string) *FailoverError {
|
||||
e.Correlation = correlation
|
||||
return e
|
||||
}
|
||||
|
||||
// ModelConfig holds primary model and fallback list.
|
||||
type ModelConfig struct {
|
||||
Primary string
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue