mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-29 08:11:32 +00:00
fix(openai): recover unavailable continuation responses
This commit is contained in:
@@ -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"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user