同步
This commit is contained in:
1 parent
e67ff26e90
commit
58f23a2773
61 files changed
+11611
-443
No files matched your search
+93
-2
@@ -25,8 +25,9 @@ type Config struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Model string
|
||||
// Proxy routes all LLM requests through the given proxy URL (http/https/socks5).
|
||||
// Empty falls back to the standard *_PROXY environment variables.
|
||||
// Proxy routes all LLM requests through the given proxy URL (http/https/socks5,
|
||||
// optionally with user:pass@ credentials). Empty means direct — it does NOT
|
||||
// fall back to the standard *_PROXY environment variables.
|
||||
Proxy string
|
||||
// RatePerSecond / RatePerMinute cap the shared request rate across ALL agents
|
||||
// using the provider (0 = that window unlimited).
|
||||
@@ -191,6 +192,96 @@ func (c Config) NewProvider() (llm.Provider, error) {
|
||||
return llm.NewProvider(lc)
|
||||
}
|
||||
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
// IsQuotaExhaustedMessage deliberately recognizes only explicit balance,
|
||||
// billing, credit, or quota-exhaustion signals. Generic 429/rate-limit text,
|
||||
// authentication failures, network errors, and server failures are excluded.
|
||||
var nonFailoverHTTPStatus = regexp.MustCompile(`(?:status(?:\s+code)?|http(?:\s+status)?)\s*[=:]?\s*(?:401|403|5\d\d)\b`)
|
||||
var transientQuotaLimit = regexp.MustCompile(`(?i)(?:\b(?:rpm|tpm|rpd|qps)\b|quota[_\s-]*metric|rate[_\s-]*limit|too many requests|(?:requests?|tokens?)\s+(?:per|/)\s*(?:second|minute)|(?:per|/)\s*(?:second|minute)\s+(?:requests?|tokens?)|generate[_\s-]*requests[_\s-]*per[_\s-]*(?:minute|second)|tokens?[_\s-]*per[_\s-]*(?:minute|second))`)
|
||||
|
||||
func IsQuotaExhaustedMessage(message string) bool {
|
||||
message = strings.ToLower(message)
|
||||
// Authentication/authorization and provider-side 5xx failures never rotate,
|
||||
// even when a gateway happens to echo a quota-looking phrase in the body.
|
||||
if nonFailoverHTTPStatus.MatchString(message) {
|
||||
return false
|
||||
}
|
||||
// Provider APIs frequently describe an ordinary rate limit as "quota
|
||||
// exceeded", especially Google-style responses containing a quota metric.
|
||||
// These limits recover with time and must stay on the current provider.
|
||||
if transientQuotaLimit.MatchString(message) {
|
||||
return false
|
||||
}
|
||||
markers := []string{
|
||||
"insufficient_quota", "quota_exceeded", "quota exceeded", "quota exhausted",
|
||||
"exceeded your current quota", "billing_hard_limit_reached",
|
||||
"billing hard limit", "billing_not_active", "credit balance", "insufficient credit",
|
||||
"insufficient balance", "balance is too low", "payment required", "status 402",
|
||||
"余额不足", "额度不足", "额度已用尽", "欠费",
|
||||
}
|
||||
for _, marker := range markers {
|
||||
if strings.Contains(message, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// gRPC RESOURCE_EXHAUSTED is overloaded for both account quota and ordinary
|
||||
// request-rate limiting. Preserve it as an explicit exhaustion signal only
|
||||
// when the same error does not identify a transient rate limit.
|
||||
return strings.Contains(message, "resource_exhausted") &&
|
||||
!strings.Contains(message, "rate limit") &&
|
||||
!strings.Contains(message, "too many requests")
|
||||
}
|
||||
|
||||
// quotaAwareTransport preserves Norma's normal retry behavior except for a 429
|
||||
// whose body explicitly says the account quota/balance is exhausted. Norma's
|
||||
// retry loop treats every 429 as transient; normalizing only that response to
|
||||
// 402 lets a task router fail over immediately while retaining the original
|
||||
// response body for provider-specific classification and audit logs.
|
||||
type quotaAwareTransport struct{ base http.RoundTripper }
|
||||
|
||||
func (t quotaAwareTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
resp, err := t.base.RoundTrip(req)
|
||||
if err != nil || resp == nil || resp.StatusCode != http.StatusTooManyRequests {
|
||||
return resp, err
|
||||
}
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
resp.Body = io.NopCloser(bytes.NewReader(body))
|
||||
resp.ContentLength = int64(len(body))
|
||||
if readErr != nil {
|
||||
return resp, nil
|
||||
}
|
||||
if IsQuotaExhaustedMessage(string(body)) {
|
||||
resp.StatusCode = http.StatusPaymentRequired
|
||||
resp.Status = "402 Payment Required"
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func quotaAwareHTTPClient(proxy string) (*http.Client, error) {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
proxy = strings.TrimSpace(proxy)
|
||||
if proxy == "" {
|
||||
transport.Proxy = nil // 留空=直连,不回退 HTTP_PROXY/HTTPS_PROXY 环境变量
|
||||
} else {
|
||||
proxyURL, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("llm: invalid proxy %q: %w", proxy, err)
|
||||
}
|
||||
switch proxyURL.Scheme {
|
||||
case "http", "https", "socks5":
|
||||
case "":
|
||||
return nil, fmt.Errorf("llm: proxy %q missing scheme (use http://, https:// or socks5://)", proxy)
|
||||
default:
|
||||
return nil, fmt.Errorf("llm: unsupported proxy scheme %q (use http, https or socks5)", proxyURL.Scheme)
|
||||
}
|
||||
transport.Proxy = http.ProxyURL(proxyURL)
|
||||
}
|
||||
return &http.Client{Transport: quotaAwareTransport{base: transport}}, nil
|
||||
}
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
// TestConnection makes a minimal real completion to verify the provider/model/
|
||||
// endpoint/key actually work. Returns the round-trip latency.
|
||||
func TestConnection(ctx context.Context, c Config) (time.Duration, error) {
|
||||
|
||||
Reference in new issue
Block a user