fix(openai): recover unavailable continuation responses

This commit is contained in:
wucm667
2026-09-03 10:26:12 +08:00
parent 5097b31457
commit b6740c937f
2 changed files with 49 additions and 5 deletions
@@ -753,7 +753,7 @@ func TestForwardAsAnthropic_PreviousResponseIDKeepsMultiToolCallContext(t *testi
require.Equal(t, "continue", gjson.GetBytes(upstream.lastBody, "input.5.content.0.text").String())
}
func TestForwardAsAnthropic_ReplaysWithoutContinuationWhenPreviousResponseMissing(t *testing.T) {
func TestForwardAsAnthropic_ReplaysFullToolHistoryWhenPreviousResponseUnavailable(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
@@ -775,12 +775,12 @@ func TestForwardAsAnthropic_ReplaysWithoutContinuationWhenPreviousResponseMissin
}
svc.bindOpenAICompatSessionResponseID(context.Background(), nil, account, "stable-cache-key", "resp_missing")
secondBody := []byte(`{"model":"claude-sonnet-4-5","max_tokens":16,"messages":[{"role":"user","content":"first"},{"role":"assistant","content":"ok"},{"role":"user","content":"second"}],"stream":false}`)
secondBody := []byte(`{"model":"claude-sonnet-4-5","max_tokens":16,"messages":[{"role":"user","content":"first"},{"role":"assistant","content":[{"type":"tool_use","id":"call_1","name":"lookup","input":{"q":"first"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"call_1","content":"found"},{"type":"text","text":"second"}]}],"tools":[{"name":"lookup","input_schema":{"type":"object"}}],"stream":false}`)
upstream.responses = []*http.Response{
{
StatusCode: http.StatusBadRequest,
Header: http.Header{"Content-Type": []string{"application/json"}, "x-request-id": []string{"rid_prev_missing"}},
Body: io.NopCloser(strings.NewReader(`{"error":{"code":"previous_response_not_found","message":"previous response not found"}}`)),
Body: io.NopCloser(strings.NewReader(`{"error":{"message":"previous_response_id is not available for this user","type":"invalid_request_error"}}`)),
},
openAICompatSSECompletedResponse("resp_replayed", "gpt-5.3-codex"),
}
@@ -797,11 +797,54 @@ func TestForwardAsAnthropic_ReplaysWithoutContinuationWhenPreviousResponseMissin
require.Len(t, upstream.requests, 2)
require.Equal(t, "resp_missing", gjson.GetBytes(upstream.bodies[0], "previous_response_id").String())
require.False(t, gjson.GetBytes(upstream.bodies[1], "previous_response_id").Exists())
require.Equal(t, int64(4), gjson.GetBytes(upstream.bodies[1], "input.#").Int())
require.Equal(t, int64(5), gjson.GetBytes(upstream.bodies[1], "input.#").Int())
require.Equal(t, "developer", gjson.GetBytes(upstream.bodies[1], "input.0.role").String())
require.Contains(t, gjson.GetBytes(upstream.bodies[1], "input.0.content.0.text").String(), "<sub2api-claude-code-todo-guard>")
require.Equal(t, "first", gjson.GetBytes(upstream.bodies[1], "input.1.content.0.text").String())
require.Equal(t, "second", gjson.GetBytes(upstream.bodies[1], "input.3.content.0.text").String())
require.Equal(t, "function_call", gjson.GetBytes(upstream.bodies[1], "input.2.type").String())
require.Equal(t, "call_1", gjson.GetBytes(upstream.bodies[1], "input.2.call_id").String())
require.Equal(t, "function_call_output", gjson.GetBytes(upstream.bodies[1], "input.3.type").String())
require.Equal(t, "call_1", gjson.GetBytes(upstream.bodies[1], "input.3.call_id").String())
require.Equal(t, "found", gjson.GetBytes(upstream.bodies[1], "input.3.output").String())
require.Equal(t, "second", gjson.GetBytes(upstream.bodies[1], "input.4.content.0.text").String())
}
func TestOpenAICompatPreviousResponseUnavailableRecognitionIsStrict(t *testing.T) {
t.Parallel()
body := []byte(`{"error":{"message":"previous_response_id is not available for this user","type":"invalid_request_error"}}`)
require.True(t, isOpenAICompatPreviousResponseNotFound(http.StatusBadRequest, "", body))
require.False(t, isOpenAICompatPreviousResponseNotFound(http.StatusForbidden, "", body))
require.False(t, isOpenAICompatPreviousResponseNotFound(http.StatusBadRequest, "permission is not available for this user", nil))
require.False(t, isOpenAICompatPreviousResponseNotFound(http.StatusBadRequest, "previous_response_id is not available for this project", nil))
}
func TestForwardAsAnthropic_PreviousResponseUnavailableRetryFailureDoesNotLoop(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
unavailable := func() *http.Response {
return &http.Response{
StatusCode: http.StatusBadRequest,
Header: http.Header{"Content-Type": []string{"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"error":{"message":"previous_response_id is not available for this user","type":"invalid_request_error"}}`)),
}
}
upstream := &httpUpstreamRecorder{responses: []*http.Response{unavailable(), unavailable()}}
svc := &OpenAIGatewayService{
httpUpstream: upstream,
cfg: &config.Config{Security: config.SecurityConfig{URLAllowlist: config.URLAllowlistConfig{Enabled: false}}},
}
account := &Account{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Concurrency: 1, Credentials: map[string]any{"api_key": "sk-test", "base_url": "https://api.openai.com/v1"}}
svc.bindOpenAICompatSessionResponseID(context.Background(), nil, account, "stable-cache-key", "resp_missing")
body := []byte(`{"model":"claude-sonnet-4-5","max_tokens":16,"messages":[{"role":"user","content":"hello"}],"stream":false}`)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", bytes.NewReader(body))
_, _ = svc.ForwardAsAnthropic(context.Background(), c, account, body, "stable-cache-key", "gpt-5.3-codex")
require.Len(t, upstream.requests, 2)
require.True(t, gjson.GetBytes(upstream.bodies[0], "previous_response_id").Exists())
require.False(t, gjson.GetBytes(upstream.bodies[1], "previous_response_id").Exists())
}
func TestForwardAsAnthropic_DisablesAPIKeyContinuationWhenUpstreamRequiresWebSocketV2(t *testing.T) {
@@ -111,6 +111,7 @@ func isOpenAICompatPreviousResponseNotFound(statusCode int, upstreamMsg string,
check := func(s string) bool {
lower := strings.ToLower(strings.TrimSpace(s))
return strings.Contains(lower, "previous_response_not_found") ||
lower == "previous_response_id is not available for this user" ||
(strings.Contains(lower, "previous response") && strings.Contains(lower, "not found")) ||
(strings.Contains(lower, "unsupported parameter") && strings.Contains(lower, "previous_response_id"))
}