fix(codex): use codex status error for websocket handshake rejections

- Use `newCodexStatusErr` when handling HTTP handshake rejections in WebSocket execution and streaming.
- Ensure rate limit retry-after metadata and error payload details are properly parsed from handshake responses.

Closes: #5270
This commit is contained in:
Luis Pater
2026-08-27 17:09:28 +08:00
parent 1cc72b9d13
commit fcea738f74
3 changed files with 96 additions and 2 deletions

View File

@@ -150,7 +150,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
return e.CodexExecutor.Execute(ctx, auth, req, opts)
}
if respHS != nil && respHS.StatusCode > 0 {
return resp, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
return resp, newCodexStatusErr(respHS.StatusCode, bodyErr)
}
helps.RecordAPIWebsocketError(ctx, e.cfg, "dial", errDial)
return resp, errDial

View File

@@ -2306,3 +2306,97 @@ func TestCodexWebsocketsExecuteStreamObservesWebSocketResponseEvents(t *testing.
t.Fatalf("Payload = %s, want used_percent 75", rateLimitEvent.Payload)
}
}
func TestCodexWebsocketsExecuteHandshakeUsageLimitReachedSetsRetryAfter(t *testing.T) {
body := []byte(`{"error":{"type":"usage_limit_reached","message":"The usage limit has been reached","resets_in_seconds":120}}`)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
if _, errWrite := w.Write(body); errWrite != nil {
t.Errorf("write handshake rejection: %v", errWrite)
}
}))
defer server.Close()
exec := NewCodexWebsocketsExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "codex-auth-quota-exhausted",
Provider: "codex",
Attributes: map[string]string{
"base_url": server.URL,
"websockets": "true",
"api_key": "sk-test",
},
}
req := cliproxyexecutor.Request{
Model: "gpt-5.6-luna",
Payload: []byte(`{"model":"gpt-5.6-luna","input":[{"type":"message","id":"msg-1"}]}`),
}
opts := cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai-response"),
ResponseFormat: sdktranslator.FromString("openai-response"),
}
_, errExecute := exec.Execute(context.Background(), auth, req, opts)
if errExecute == nil {
t.Fatal("Execute() error = nil, want handshake rejection")
}
statusErr, ok := errExecute.(interface{ StatusCode() int })
if !ok || statusErr.StatusCode() != http.StatusTooManyRequests {
t.Fatalf("status = %#v, want 429", errExecute)
}
retryable, ok := errExecute.(interface{ RetryAfter() *time.Duration })
if !ok || retryable.RetryAfter() == nil {
t.Fatalf("expected RetryAfter for usage_limit_reached handshake error: %#v", errExecute)
}
if got := *retryable.RetryAfter(); got != 120*time.Second {
t.Fatalf("RetryAfter = %v, want 120s", got)
}
}
func TestCodexWebsocketsExecuteStreamHandshakeUsageLimitReachedSetsRetryAfter(t *testing.T) {
body := []byte(`{"error":{"type":"usage_limit_reached","message":"The usage limit has been reached","resets_in_seconds":120}}`)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
if _, errWrite := w.Write(body); errWrite != nil {
t.Errorf("write handshake rejection: %v", errWrite)
}
}))
defer server.Close()
exec := NewCodexWebsocketsExecutor(&config.Config{})
auth := &cliproxyauth.Auth{
ID: "codex-auth-quota-exhausted-stream",
Provider: "codex",
Attributes: map[string]string{
"base_url": server.URL,
"websockets": "true",
"api_key": "sk-test",
},
}
req := cliproxyexecutor.Request{
Model: "gpt-5.6-luna",
Payload: []byte(`{"model":"gpt-5.6-luna","input":[{"type":"message","id":"msg-1"}]}`),
}
opts := cliproxyexecutor.Options{
SourceFormat: sdktranslator.FromString("openai-response"),
ResponseFormat: sdktranslator.FromString("openai-response"),
}
_, errExecuteStream := exec.ExecuteStream(context.Background(), auth, req, opts)
if errExecuteStream == nil {
t.Fatal("ExecuteStream() error = nil, want handshake rejection")
}
statusErr, ok := errExecuteStream.(interface{ StatusCode() int })
if !ok || statusErr.StatusCode() != http.StatusTooManyRequests {
t.Fatalf("status = %#v, want 429", errExecuteStream)
}
retryable, ok := errExecuteStream.(interface{ RetryAfter() *time.Duration })
if !ok || retryable.RetryAfter() == nil {
t.Fatalf("expected RetryAfter for usage_limit_reached handshake error: %#v", errExecuteStream)
}
if got := *retryable.RetryAfter(); got != 120*time.Second {
t.Fatalf("RetryAfter = %v, want 120s", got)
}
}

View File

@@ -158,7 +158,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
if sess != nil {
sess.reqMu.Unlock()
}
return nil, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
return nil, newCodexStatusErr(respHS.StatusCode, bodyErr)
}
helps.RecordAPIWebsocketError(ctx, e.cfg, "dial", errDial)
if sess != nil {