Merge pull request #22 from hobbyistlabs-coder/fix/granular-error-classification-12017654499930892984
🧹 Refactor context length error handling to use providers error classification
This commit is contained in:
commit
30ddae62c7
4 changed files with 72 additions and 28 deletions
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"jane/pkg/bus"
|
"jane/pkg/bus"
|
||||||
|
|
@ -77,25 +76,27 @@ func (al *AgentLoop) executeLLMWithRetry(
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
errMsg := strings.ToLower(err.Error())
|
isTimeoutError := false
|
||||||
|
isContextError := false
|
||||||
|
|
||||||
// Check if this is a network/HTTP timeout — not a context window error.
|
var failErr *providers.FailoverError
|
||||||
isTimeoutError := errors.Is(err, context.DeadlineExceeded) ||
|
if errors.As(err, &failErr) {
|
||||||
strings.Contains(errMsg, "deadline exceeded") ||
|
if failErr.Reason == providers.FailoverTimeout {
|
||||||
strings.Contains(errMsg, "client.timeout") ||
|
isTimeoutError = true
|
||||||
strings.Contains(errMsg, "timed out") ||
|
} else if failErr.Reason == providers.FailoverContextLength {
|
||||||
strings.Contains(errMsg, "timeout exceeded")
|
isContextError = true
|
||||||
|
}
|
||||||
// Detect real context window / token limit errors, excluding network timeouts.
|
} else {
|
||||||
isContextError := !isTimeoutError && (strings.Contains(errMsg, "context_length_exceeded") ||
|
// If not a fallback error, check directly using ClassifyError
|
||||||
strings.Contains(errMsg, "context window") ||
|
// The provider might not be wrapped if no fallback chain is active
|
||||||
strings.Contains(errMsg, "maximum context length") ||
|
if directFailErr := providers.ClassifyError(err, "", ""); directFailErr != nil {
|
||||||
strings.Contains(errMsg, "token limit") ||
|
if directFailErr.Reason == providers.FailoverTimeout {
|
||||||
strings.Contains(errMsg, "too many tokens") ||
|
isTimeoutError = true
|
||||||
strings.Contains(errMsg, "max_tokens") ||
|
} else if directFailErr.Reason == providers.FailoverContextLength {
|
||||||
strings.Contains(errMsg, "invalidparameter") ||
|
isContextError = true
|
||||||
strings.Contains(errMsg, "prompt is too long") ||
|
}
|
||||||
strings.Contains(errMsg, "request too large"))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isTimeoutError && retry < maxRetries {
|
if isTimeoutError && retry < maxRetries {
|
||||||
// Exponential backoff: 2s, 4s, 8s
|
// Exponential backoff: 2s, 4s, 8s
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,18 @@ var (
|
||||||
substr("invalid request format"),
|
substr("invalid request format"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contextLengthPatterns = []errorPattern{
|
||||||
|
substr("context_length_exceeded"),
|
||||||
|
substr("context window"),
|
||||||
|
substr("maximum context length"),
|
||||||
|
substr("token limit"),
|
||||||
|
substr("too many tokens"),
|
||||||
|
substr("max_tokens"),
|
||||||
|
substr("invalidparameter"),
|
||||||
|
substr("prompt is too long"),
|
||||||
|
substr("request too large"),
|
||||||
|
}
|
||||||
|
|
||||||
imageDimensionPatterns = []errorPattern{
|
imageDimensionPatterns = []errorPattern{
|
||||||
rxp(`image dimensions exceed max`),
|
rxp(`image dimensions exceed max`),
|
||||||
}
|
}
|
||||||
|
|
@ -201,6 +213,9 @@ func classifyByMessage(msg string) FailoverReason {
|
||||||
if matchesAny(msg, formatPatterns) {
|
if matchesAny(msg, formatPatterns) {
|
||||||
return FailoverFormat
|
return FailoverFormat
|
||||||
}
|
}
|
||||||
|
if matchesAny(msg, contextLengthPatterns) {
|
||||||
|
return FailoverContextLength
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,32 @@ func TestClassifyError_FormatPatterns(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClassifyError_ContextLengthPatterns(t *testing.T) {
|
||||||
|
patterns := []string{
|
||||||
|
"context_length_exceeded",
|
||||||
|
"context window reached",
|
||||||
|
"maximum context length exceeded",
|
||||||
|
"token limit reached",
|
||||||
|
"too many tokens for this model",
|
||||||
|
"max_tokens limit hit",
|
||||||
|
"invalidparameter",
|
||||||
|
"prompt is too long",
|
||||||
|
"request too large",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, msg := range patterns {
|
||||||
|
err := errors.New(msg)
|
||||||
|
result := ClassifyError(err, "anthropic", "claude")
|
||||||
|
if result == nil {
|
||||||
|
t.Errorf("pattern %q: expected non-nil", msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if result.Reason != FailoverContextLength {
|
||||||
|
t.Errorf("pattern %q: reason = %q, want context_length", msg, result.Reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClassifyError_ImageDimensionError(t *testing.T) {
|
func TestClassifyError_ImageDimensionError(t *testing.T) {
|
||||||
err := errors.New("image dimensions exceed max allowed 2048x2048")
|
err := errors.New("image dimensions exceed max allowed 2048x2048")
|
||||||
result := ClassifyError(err, "openai", "gpt-4o")
|
result := ClassifyError(err, "openai", "gpt-4o")
|
||||||
|
|
@ -265,6 +291,7 @@ func TestFailoverError_IsRetriable(t *testing.T) {
|
||||||
{FailoverTimeout, true},
|
{FailoverTimeout, true},
|
||||||
{FailoverOverloaded, true},
|
{FailoverOverloaded, true},
|
||||||
{FailoverFormat, false},
|
{FailoverFormat, false},
|
||||||
|
{FailoverContextLength, false},
|
||||||
{FailoverUnknown, true},
|
{FailoverUnknown, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ const (
|
||||||
FailoverBilling FailoverReason = "billing"
|
FailoverBilling FailoverReason = "billing"
|
||||||
FailoverTimeout FailoverReason = "timeout"
|
FailoverTimeout FailoverReason = "timeout"
|
||||||
FailoverFormat FailoverReason = "format"
|
FailoverFormat FailoverReason = "format"
|
||||||
|
FailoverContextLength FailoverReason = "context_length"
|
||||||
FailoverOverloaded FailoverReason = "overloaded"
|
FailoverOverloaded FailoverReason = "overloaded"
|
||||||
FailoverUnknown FailoverReason = "unknown"
|
FailoverUnknown FailoverReason = "unknown"
|
||||||
)
|
)
|
||||||
|
|
@ -76,9 +77,9 @@ func (e *FailoverError) Unwrap() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRetriable returns true if this error should trigger fallback to next candidate.
|
// IsRetriable returns true if this error should trigger fallback to next candidate.
|
||||||
// Non-retriable: Format errors (bad request structure, image dimension/size).
|
// Non-retriable: Format errors (bad request structure, image dimension/size), Context length exceeded.
|
||||||
func (e *FailoverError) IsRetriable() bool {
|
func (e *FailoverError) IsRetriable() bool {
|
||||||
return e.Reason != FailoverFormat
|
return e.Reason != FailoverFormat && e.Reason != FailoverContextLength
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelConfig holds primary model and fallback list.
|
// ModelConfig holds primary model and fallback list.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue