24e072b63d
Replaces the Mistral-only ai.Client with an ai.Provider interface backed by Ollama and Mistral implementations. Migrates enrichment + similarity callers to ai.Provider.Chat. Research endpoint returns 501 until commit 2 reinstates it on the new orchestrator.
100 lines
2.4 KiB
Go
100 lines
2.4 KiB
Go
package ai
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
type ErrorCode int
|
|
|
|
const (
|
|
ErrInternal ErrorCode = iota
|
|
ErrRateLimited
|
|
ErrQuotaExceeded
|
|
ErrTimeout
|
|
ErrInvalidRequest
|
|
ErrUnavailable
|
|
ErrSchemaViolation
|
|
)
|
|
|
|
func (c ErrorCode) String() string {
|
|
switch c {
|
|
case ErrInternal:
|
|
return "internal"
|
|
case ErrRateLimited:
|
|
return "rate_limited"
|
|
case ErrQuotaExceeded:
|
|
return "quota_exceeded"
|
|
case ErrTimeout:
|
|
return "timeout"
|
|
case ErrInvalidRequest:
|
|
return "invalid_request"
|
|
case ErrUnavailable:
|
|
return "unavailable"
|
|
case ErrSchemaViolation:
|
|
return "schema_violation"
|
|
default:
|
|
return "internal"
|
|
}
|
|
}
|
|
|
|
type ProviderError struct {
|
|
Code ErrorCode
|
|
Message string
|
|
Retryable bool
|
|
Inner error
|
|
RawOutput string
|
|
}
|
|
|
|
func (e *ProviderError) Error() string {
|
|
if e.Inner != nil {
|
|
return fmt.Sprintf("ai: %s: %s: %v", e.Code, e.Message, e.Inner)
|
|
}
|
|
return fmt.Sprintf("ai: %s: %s", e.Code, e.Message)
|
|
}
|
|
|
|
func (e *ProviderError) Unwrap() error { return e.Inner }
|
|
|
|
func ClassifyError(err error) *ProviderError {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var pe *ProviderError
|
|
if errors.As(err, &pe) {
|
|
return pe
|
|
}
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return &ProviderError{Code: ErrTimeout, Message: "context deadline exceeded", Retryable: true, Inner: err}
|
|
}
|
|
|
|
msg := strings.ToLower(err.Error())
|
|
switch {
|
|
case strings.Contains(msg, "429"),
|
|
strings.Contains(msg, "too many requests"),
|
|
strings.Contains(msg, "rate limit"):
|
|
return &ProviderError{Code: ErrRateLimited, Message: err.Error(), Retryable: true, Inner: err}
|
|
case strings.Contains(msg, "deadline exceeded"),
|
|
strings.Contains(msg, "timeout"):
|
|
return &ProviderError{Code: ErrTimeout, Message: err.Error(), Retryable: true, Inner: err}
|
|
case strings.Contains(msg, "connection refused"),
|
|
strings.Contains(msg, "no such host"),
|
|
isNetError(err):
|
|
return &ProviderError{Code: ErrUnavailable, Message: err.Error(), Retryable: true, Inner: err}
|
|
case strings.Contains(msg, "quota"),
|
|
strings.Contains(msg, "insufficient"):
|
|
return &ProviderError{Code: ErrQuotaExceeded, Message: err.Error(), Retryable: false, Inner: err}
|
|
case strings.Contains(msg, "400"),
|
|
strings.Contains(msg, "invalid"):
|
|
return &ProviderError{Code: ErrInvalidRequest, Message: err.Error(), Retryable: false, Inner: err}
|
|
}
|
|
return &ProviderError{Code: ErrInternal, Message: err.Error(), Retryable: false, Inner: err}
|
|
}
|
|
|
|
func isNetError(err error) bool {
|
|
var ne net.Error
|
|
return errors.As(err, &ne)
|
|
}
|