mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 17:34:39 +08:00
fix: increase x402 payment timeout to 5min and add 402 re-sign logic
AI inference (especially DeepSeek) often exceeds the default 120s HTTP timeout, causing the client to disconnect while the server completes successfully — resulting in repeated payments on each retry. Changes: - Set X402Timeout = 5min for all x402 providers (Claw402, BlockRunBase, BlockRunSol) - Handle 402 during payment retry by re-extracting Payment-Required header and re-signing instead of failing immediately - Increase payment retry attempts from 3 to 5 for unstable gateways
This commit is contained in:
@@ -72,6 +72,7 @@ func NewBlockRunBaseClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
|||||||
mcp.WithProvider(mcp.ProviderBlockRunBase),
|
mcp.WithProvider(mcp.ProviderBlockRunBase),
|
||||||
mcp.WithModel(DefaultBlockRunModel),
|
mcp.WithModel(DefaultBlockRunModel),
|
||||||
mcp.WithBaseURL(DefaultBlockRunBaseURL),
|
mcp.WithBaseURL(DefaultBlockRunBaseURL),
|
||||||
|
mcp.WithTimeout(X402Timeout),
|
||||||
}
|
}
|
||||||
allOpts := append(baseOpts, opts...)
|
allOpts := append(baseOpts, opts...)
|
||||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ func NewBlockRunSolClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
|||||||
mcp.WithProvider(mcp.ProviderBlockRunSol),
|
mcp.WithProvider(mcp.ProviderBlockRunSol),
|
||||||
mcp.WithModel(DefaultBlockRunModel),
|
mcp.WithModel(DefaultBlockRunModel),
|
||||||
mcp.WithBaseURL(DefaultBlockRunSolURL),
|
mcp.WithBaseURL(DefaultBlockRunSolURL),
|
||||||
|
mcp.WithTimeout(X402Timeout),
|
||||||
}
|
}
|
||||||
allOpts := append(baseOpts, opts...)
|
allOpts := append(baseOpts, opts...)
|
||||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ func NewClaw402ClientWithOptions(opts ...mcp.ClientOption) mcp.AIClient {
|
|||||||
mcp.WithProvider(mcp.ProviderClaw402),
|
mcp.WithProvider(mcp.ProviderClaw402),
|
||||||
mcp.WithModel(DefaultClaw402Model),
|
mcp.WithModel(DefaultClaw402Model),
|
||||||
mcp.WithBaseURL(DefaultClaw402URL),
|
mcp.WithBaseURL(DefaultClaw402URL),
|
||||||
|
mcp.WithTimeout(X402Timeout),
|
||||||
}
|
}
|
||||||
allOpts := append(baseOpts, opts...)
|
allOpts := append(baseOpts, opts...)
|
||||||
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
baseClient := mcp.NewClient(allOpts...).(*mcp.Client)
|
||||||
|
|||||||
@@ -16,12 +16,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// X402MaxPaymentRetries is the number of retries for 5xx errors on the
|
// X402MaxPaymentRetries is the number of retries for 5xx/expired-402 errors
|
||||||
// payment-signed request. The same payment signature is reused (no double-charge).
|
// on the payment-signed request. Payment is re-signed on 402 (no double-charge).
|
||||||
X402MaxPaymentRetries = 3
|
X402MaxPaymentRetries = 5
|
||||||
|
|
||||||
// X402RetryBaseWait is the base wait between payment retry attempts.
|
// X402RetryBaseWait is the base wait between payment retry attempts.
|
||||||
X402RetryBaseWait = 3 * time.Second
|
X402RetryBaseWait = 3 * time.Second
|
||||||
|
|
||||||
|
// X402Timeout is the HTTP timeout for x402 payment providers.
|
||||||
|
// AI inference (especially DeepSeek) can take several minutes; the default
|
||||||
|
// 120s causes premature timeouts that trigger duplicate payments.
|
||||||
|
X402Timeout = 5 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Shared x402 types ────────────────────────────────────────────────────────
|
// ── Shared x402 types ────────────────────────────────────────────────────────
|
||||||
@@ -131,7 +136,7 @@ func DoX402Request(
|
|||||||
return nil, fmt.Errorf("failed to sign x402 payment: %w", err)
|
return nil, fmt.Errorf("failed to sign x402 payment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry loop for 5xx errors on the payment-signed request.
|
// Retry loop for 5xx / expired-402 errors on the payment-signed request.
|
||||||
var lastBody []byte
|
var lastBody []byte
|
||||||
var lastStatus int
|
var lastStatus int
|
||||||
for attempt := 1; attempt <= X402MaxPaymentRetries; attempt++ {
|
for attempt := 1; attempt <= X402MaxPaymentRetries; attempt++ {
|
||||||
@@ -173,16 +178,41 @@ func DoX402Request(
|
|||||||
lastBody = body2
|
lastBody = body2
|
||||||
lastStatus = resp2.StatusCode
|
lastStatus = resp2.StatusCode
|
||||||
|
|
||||||
// Retry on 5xx server errors
|
retryable := resp2.StatusCode >= 500 || resp2.StatusCode == http.StatusPaymentRequired
|
||||||
if resp2.StatusCode >= 500 && attempt < X402MaxPaymentRetries {
|
|
||||||
|
if retryable && attempt < X402MaxPaymentRetries {
|
||||||
wait := X402RetryBaseWait * time.Duration(attempt)
|
wait := X402RetryBaseWait * time.Duration(attempt)
|
||||||
logger.Warnf("⚠️ [%s] Server error (status %d), retrying in %v (%d/%d)...",
|
|
||||||
providerTag, resp2.StatusCode, wait, attempt+1, X402MaxPaymentRetries)
|
// If we got 402 again, the payment signature expired — re-sign.
|
||||||
|
if resp2.StatusCode == http.StatusPaymentRequired {
|
||||||
|
newHeader := resp2.Header.Get("Payment-Required")
|
||||||
|
if newHeader == "" {
|
||||||
|
newHeader = resp2.Header.Get("X-Payment-Required")
|
||||||
|
}
|
||||||
|
if newHeader != "" {
|
||||||
|
newSig, signErr := signFn(newHeader)
|
||||||
|
if signErr == nil {
|
||||||
|
paymentSig = newSig
|
||||||
|
logger.Warnf("⚠️ [%s] Payment expired (402), re-signed and retrying in %v (%d/%d)...",
|
||||||
|
providerTag, wait, attempt+1, X402MaxPaymentRetries)
|
||||||
|
} else {
|
||||||
|
logger.Warnf("⚠️ [%s] Payment expired (402), re-sign failed: %v, retrying in %v (%d/%d)...",
|
||||||
|
providerTag, signErr, wait, attempt+1, X402MaxPaymentRetries)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Warnf("⚠️ [%s] Got 402 but no new Payment-Required header, retrying in %v (%d/%d)...",
|
||||||
|
providerTag, wait, attempt+1, X402MaxPaymentRetries)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Warnf("⚠️ [%s] Server error (status %d), retrying in %v (%d/%d)...",
|
||||||
|
providerTag, resp2.StatusCode, wait, attempt+1, X402MaxPaymentRetries)
|
||||||
|
}
|
||||||
|
|
||||||
time.Sleep(wait)
|
time.Sleep(wait)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-5xx error or final attempt — fail
|
// Non-retryable error or final attempt — fail
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user