返回 DeepSeek-Reasonix
quota_error.go
根目录 / internal / provider / quota_error.go
1 package provider
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "strings"
8 )
9
10 // QuotaError distinguishes exhausted credits/entitlement from invalid keys,
11 // including gateways which encode billing failures as HTTP 401 or 429. Raw
12 // bodies may contain private billing URLs or credentials and are not retained.
13 type QuotaError struct {
14 Provider string
15 ProviderDisplayName string
16 Protocol string
17 Status int
18 Code string
19 }
20
21 func (e *QuotaError) Error() string {
22 return fmt.Sprintf("provider %q credits or subscription quota exhausted (HTTP %d); check account allowance before continuing", ProviderDisplayLabel(e.Provider, e.ProviderDisplayName, e.Protocol), e.Status)
23 }
24
25 // Unwrap preserves status-based APIError consumers without exposing the raw
26 // billing response or making a quota rejection look like an AuthError.
27 func (e *QuotaError) Unwrap() error {
28 return &APIError{Provider: e.Provider, ProviderDisplayName: e.ProviderDisplayName, Protocol: e.Protocol, Status: e.Status}
29 }
30
31 func QuotaErrorFromResponse(name string, status int, body string) *QuotaError {
32 return quotaErrorFromResponse(name, "", "", status, body)
33 }
34
35 func QuotaErrorFromResponseWithIdentity(name, displayName, protocol string, status int, body string) *QuotaError {
36 return quotaErrorFromResponse(name, displayName, protocol, status, body)
37 }
38
39 func quotaErrorFromResponse(name, displayName, protocol string, status int, body string) *QuotaError {
40 var v struct {
41 Error struct {
42 Code string `json:"code"`
43 Type string `json:"type"`
44 Message string `json:"message"`
45 } `json:"error"`
46 }
47 _ = json.Unmarshal([]byte(body), &v)
48 code := v.Error.Code
49 if code == "" {
50 code = v.Error.Type
51 }
52 lower := strings.ToLower(body)
53 for _, marker := range []string{"insufficient_quota", "insufficient balance", "insufficient token quota", "out of budget", "quota exceeded", "freeusagelimiterror", "gousagelimiterror", "creditserror", "monthly usage limit reached", "available balance"} {
54 if strings.Contains(lower, marker) {
55 return &QuotaError{Provider: name, ProviderDisplayName: displayName, Protocol: protocol, Status: status, Code: code}
56 }
57 }
58 if status == 402 {
59 return &QuotaError{Provider: name, ProviderDisplayName: displayName, Protocol: protocol, Status: status, Code: code}
60 }
61 return nil
62 }
63
64 // AsQuotaError also handles legacy adapters returning structured API/Auth errors.
65 func AsQuotaError(err error) *QuotaError {
66 var quota *QuotaError
67 if errors.As(err, &quota) {
68 return quota
69 }
70 var api *APIError
71 if errors.As(err, &api) {
72 return QuotaErrorFromResponseWithIdentity(api.Provider, api.ProviderDisplayName, api.Protocol, api.Status, api.Body)
73 }
74 var auth *AuthError
75 if errors.As(err, &auth) {
76 return QuotaErrorFromResponseWithIdentity(auth.Provider, auth.ProviderDisplayName, auth.Protocol, auth.Status, auth.Body)
77 }
78 return nil
79 }
80
80 lines GO