mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-20 22:51:45 +00:00
Cursor executor errors were plain fmt.Errorf — the conductor couldn't
extract HTTP status codes, so exhausted accounts never entered cooldown.
Changes:
- Add ConnectError struct to proto/connect.go: ParseConnectEndStream now
returns *ConnectError with Code/Message fields for precise matching
- Add cursorStatusErr implementing StatusError + RetryAfter interfaces
- Add classifyCursorError() with two-layer classification:
Layer 1: exact match on ConnectError.Code (gRPC standard codes)
resource_exhausted → 429, unauthenticated → 401,
permission_denied → 403, unavailable → 503, internal → 500
Layer 2: fuzzy string match for H2 errors (RST_STREAM → 502)
- Log all ConnectError code/message pairs for observing real server
error codes (we have no samples yet)
- Wrap Execute and ExecuteStream error returns with classifyCursorError
Now the conductor properly marks Cursor auths as cooldown on quota errors,
enabling exponential backoff and round-robin failover.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
85 lines
2.5 KiB
Go
85 lines
2.5 KiB
Go
package proto
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
const (
|
|
// ConnectEndStreamFlag marks the end-of-stream frame (trailers).
|
|
ConnectEndStreamFlag byte = 0x02
|
|
// ConnectCompressionFlag indicates the payload is compressed (not supported).
|
|
ConnectCompressionFlag byte = 0x01
|
|
// ConnectFrameHeaderSize is the fixed 5-byte frame header.
|
|
ConnectFrameHeaderSize = 5
|
|
)
|
|
|
|
// FrameConnectMessage wraps a protobuf payload in a Connect frame.
|
|
// Frame format: [1 byte flags][4 bytes payload length (big-endian)][payload]
|
|
func FrameConnectMessage(data []byte, flags byte) []byte {
|
|
frame := make([]byte, ConnectFrameHeaderSize+len(data))
|
|
frame[0] = flags
|
|
binary.BigEndian.PutUint32(frame[1:5], uint32(len(data)))
|
|
copy(frame[5:], data)
|
|
return frame
|
|
}
|
|
|
|
// ParseConnectFrame extracts one frame from a buffer.
|
|
// Returns (flags, payload, bytesConsumed, ok).
|
|
// ok is false when the buffer is too short for a complete frame.
|
|
func ParseConnectFrame(buf []byte) (flags byte, payload []byte, consumed int, ok bool) {
|
|
if len(buf) < ConnectFrameHeaderSize {
|
|
return 0, nil, 0, false
|
|
}
|
|
flags = buf[0]
|
|
length := binary.BigEndian.Uint32(buf[1:5])
|
|
total := ConnectFrameHeaderSize + int(length)
|
|
if len(buf) < total {
|
|
return 0, nil, 0, false
|
|
}
|
|
return flags, buf[5:total], total, true
|
|
}
|
|
|
|
// ConnectError is a structured error from the Connect protocol end-of-stream trailer.
|
|
// The Code field contains the server-defined error code (e.g. gRPC standard codes
|
|
// like "resource_exhausted", "unauthenticated", "permission_denied", "unavailable").
|
|
type ConnectError struct {
|
|
Code string // server-defined error code
|
|
Message string // human-readable error description
|
|
}
|
|
|
|
func (e *ConnectError) Error() string {
|
|
return fmt.Sprintf("Connect error %s: %s", e.Code, e.Message)
|
|
}
|
|
|
|
// ParseConnectEndStream parses a Connect end-of-stream frame payload (JSON).
|
|
// Returns nil if there is no error in the trailer.
|
|
// On error, returns a *ConnectError with the server's error code and message.
|
|
func ParseConnectEndStream(data []byte) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
var trailer struct {
|
|
Error *struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(data, &trailer); err != nil {
|
|
return fmt.Errorf("failed to parse Connect end stream: %w", err)
|
|
}
|
|
if trailer.Error != nil {
|
|
code := trailer.Error.Code
|
|
if code == "" {
|
|
code = "unknown"
|
|
}
|
|
msg := trailer.Error.Message
|
|
if msg == "" {
|
|
msg = "Unknown error"
|
|
}
|
|
return &ConnectError{Code: code, Message: msg}
|
|
}
|
|
return nil
|
|
}
|