Retry transient OpenAI Codex processing errors
- commit
- 079dd65ac786619c9fdf2e87247446a3ff74e336
- author
- patriceckhart · · 2026-07-29 16:16 UTC
- parent
- 100f6a2
- signature
- unsigned
- refs
- tag: v0.3.24
2 files · +51 -0
M packages/core/agent.go +7 -0
| @@ -471,6 +471,13 @@ func (a *Agent) canRetryError(err error, attempt int) bool { | |||
| 471 | 471 | "connection refused", "connection lost", "fetch failed", "upstream connect", "reset before headers", | |
| 472 | 472 | "socket hang up", "ended without", "stream ended before", "did not get a response", "timed out", | |
| 473 | 473 | "timeout", "terminated", "unexpected eof", "transport failure", | |
| 474 | + | // OpenAI's ChatGPT/Codex backend returns this generic message (with a | |
| 475 | + | // request ID) for transient server failures and explicitly says | |
| 476 | + | // "You can retry your request". | |
| 477 | + | "an error occurred while processing your request", | |
| 478 | + | // Explicit retry guidance emitted by provider backends (OpenAI | |
| 479 | + | // Responses, AWS Bedrock stream exceptions) with varying prefixes. | |
| 480 | + | "you can retry your request", "try your request again", "please retry your request", | |
| 474 | 481 | } | |
| 475 | 482 | for _, needle := range needles { | |
| 476 | 483 | if strings.Contains(msg, needle) { | |
| @@ -471,6 +471,13 @@ func (a *Agent) canRetryError(err error, attempt int) bool { | |||||
| 471 | "connection refused", "connection lost", "fetch failed", "upstream connect", "reset before headers", | 471 | "connection refused", "connection lost", "fetch failed", "upstream connect", "reset before headers", | ||
| 472 | "socket hang up", "ended without", "stream ended before", "did not get a response", "timed out", | 472 | "socket hang up", "ended without", "stream ended before", "did not get a response", "timed out", | ||
| 473 | "timeout", "terminated", "unexpected eof", "transport failure", | 473 | "timeout", "terminated", "unexpected eof", "transport failure", | ||
| 474 | + | // OpenAI's ChatGPT/Codex backend returns this generic message (with a | |||
| 475 | + | // request ID) for transient server failures and explicitly says | |||
| 476 | + | // "You can retry your request". | |||
| 477 | + | "an error occurred while processing your request", | |||
| 478 | + | // Explicit retry guidance emitted by provider backends (OpenAI | |||
| 479 | + | // Responses, AWS Bedrock stream exceptions) with varying prefixes. | |||
| 480 | + | "you can retry your request", "try your request again", "please retry your request", | |||
| 474 | } | 481 | } | ||
| 475 | for _, needle := range needles { | 482 | for _, needle := range needles { | ||
| 476 | if strings.Contains(msg, needle) { | 483 | if strings.Contains(msg, needle) { | ||
M packages/core/agent_retry_test.go +44 -0
| @@ -65,6 +65,50 @@ func TestAgentRetriesOverloadedStreamError(t *testing.T) { | |||
| 65 | 65 | } | |
| 66 | 66 | } | |
| 67 | 67 | ||
| 68 | + | // codexRetryFakeClient reproduces the generic OpenAI Codex backend | |
| 69 | + | // failure ("An error occurred while processing your request. You can | |
| 70 | + | // retry your request, ...") on the first call and succeeds afterwards. | |
| 71 | + | type codexRetryFakeClient struct { | |
| 72 | + | calls int32 | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (c *codexRetryFakeClient) Name() string { return "openai-codex" } | |
| 76 | + | ||
| 77 | + | func (c *codexRetryFakeClient) Stream(ctx context.Context, req provider.Request) (<-chan provider.Event, error) { | |
| 78 | + | call := atomic.AddInt32(&c.calls, 1) | |
| 79 | + | out := make(chan provider.Event, 4) | |
| 80 | + | go func() { | |
| 81 | + | defer close(out) | |
| 82 | + | out <- provider.EventStart{Provider: "openai-codex", Model: req.Model} | |
| 83 | + | if call == 1 { | |
| 84 | + | out <- provider.EventDone{Stop: provider.StopError, Err: fmt.Errorf("codex error: An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID 60c8ebbd-20bd-42e4-b756-6e844041cfc0 in your message.")} | |
| 85 | + | return | |
| 86 | + | } | |
| 87 | + | out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{ | |
| 88 | + | Role: provider.RoleAssistant, | |
| 89 | + | Content: []provider.Content{provider.TextBlock{Text: "ok"}}, | |
| 90 | + | }} | |
| 91 | + | }() | |
| 92 | + | return out, nil | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | func TestAgentRetriesCodexProcessingError(t *testing.T) { | |
| 96 | + | client := &codexRetryFakeClient{} | |
| 97 | + | a := NewAgent(client, "gpt-5.6-sol", "system", Registry{}) | |
| 98 | + | a.RetryBaseDelay = time.Millisecond | |
| 99 | + | ||
| 100 | + | if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil { | |
| 101 | + | t.Fatalf("Prompt returned %v; want retry to succeed", err) | |
| 102 | + | } | |
| 103 | + | if got := atomic.LoadInt32(&client.calls); got != 2 { | |
| 104 | + | t.Fatalf("Stream calls = %d; want 2", got) | |
| 105 | + | } | |
| 106 | + | msgs := a.Messages() | |
| 107 | + | if len(msgs) != 2 || extractText(msgs[1]) != "ok" { | |
| 108 | + | t.Fatalf("messages = %v; want user + ok assistant", msgs) | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | ||
| 68 | 112 | type partialRetryFakeClient struct { | |
| 69 | 113 | calls int32 | |
| 70 | 114 | } | |
| @@ -65,6 +65,50 @@ func TestAgentRetriesOverloadedStreamError(t *testing.T) { | |||||
| 65 | } | 65 | } | ||
| 66 | } | 66 | } | ||
| 67 | 67 | ||||
| 68 | + | // codexRetryFakeClient reproduces the generic OpenAI Codex backend | |||
| 69 | + | // failure ("An error occurred while processing your request. You can | |||
| 70 | + | // retry your request, ...") on the first call and succeeds afterwards. | |||
| 71 | + | type codexRetryFakeClient struct { | |||
| 72 | + | calls int32 | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (c *codexRetryFakeClient) Name() string { return "openai-codex" } | |||
| 76 | + | ||||
| 77 | + | func (c *codexRetryFakeClient) Stream(ctx context.Context, req provider.Request) (<-chan provider.Event, error) { | |||
| 78 | + | call := atomic.AddInt32(&c.calls, 1) | |||
| 79 | + | out := make(chan provider.Event, 4) | |||
| 80 | + | go func() { | |||
| 81 | + | defer close(out) | |||
| 82 | + | out <- provider.EventStart{Provider: "openai-codex", Model: req.Model} | |||
| 83 | + | if call == 1 { | |||
| 84 | + | out <- provider.EventDone{Stop: provider.StopError, Err: fmt.Errorf("codex error: An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID 60c8ebbd-20bd-42e4-b756-6e844041cfc0 in your message.")} | |||
| 85 | + | return | |||
| 86 | + | } | |||
| 87 | + | out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{ | |||
| 88 | + | Role: provider.RoleAssistant, | |||
| 89 | + | Content: []provider.Content{provider.TextBlock{Text: "ok"}}, | |||
| 90 | + | }} | |||
| 91 | + | }() | |||
| 92 | + | return out, nil | |||
| 93 | + | } | |||
| 94 | + | ||||
| 95 | + | func TestAgentRetriesCodexProcessingError(t *testing.T) { | |||
| 96 | + | client := &codexRetryFakeClient{} | |||
| 97 | + | a := NewAgent(client, "gpt-5.6-sol", "system", Registry{}) | |||
| 98 | + | a.RetryBaseDelay = time.Millisecond | |||
| 99 | + | ||||
| 100 | + | if err := a.Prompt(context.Background(), "hello", nil, nil); err != nil { | |||
| 101 | + | t.Fatalf("Prompt returned %v; want retry to succeed", err) | |||
| 102 | + | } | |||
| 103 | + | if got := atomic.LoadInt32(&client.calls); got != 2 { | |||
| 104 | + | t.Fatalf("Stream calls = %d; want 2", got) | |||
| 105 | + | } | |||
| 106 | + | msgs := a.Messages() | |||
| 107 | + | if len(msgs) != 2 || extractText(msgs[1]) != "ok" { | |||
| 108 | + | t.Fatalf("messages = %v; want user + ok assistant", msgs) | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | ||||
| 68 | type partialRetryFakeClient struct { | 112 | type partialRetryFakeClient struct { | ||
| 69 | calls int32 | 113 | calls int32 | ||
| 70 | } | 114 | } | ||