diff --git a/go.mod b/go.mod index 43ba48c76..b7957e993 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/pion/sdp/v3 v3.0.20 github.com/pion/stun/v3 v3.1.7 github.com/pion/webrtc/v4 v4.2.22 - github.com/router-for-me/CLIProxyAPI/v8 v8.0.22 + github.com/router-for-me/CLIProxyAPI/v8 v8.0.23 github.com/router-for-me/CLIProxyAPI/v8/gptload-embedded v0.0.0 github.com/sirupsen/logrus v1.10.2 github.com/tidwall/gjson v1.19.0 diff --git a/go.sum b/go.sum index e4d92f18d..d042b6631 100644 --- a/go.sum +++ b/go.sum @@ -245,8 +245,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= -github.com/router-for-me/CLIProxyAPI/v8 v8.0.22 h1:KX7c+ThdMlhG+SiDSYrMLByAbaqhU4Azl7yR9ihu2HY= -github.com/router-for-me/CLIProxyAPI/v8 v8.0.22/go.mod h1:j1Mtcj0TrC66O1JwbSBB03lY3uYPx4J7yHNvU6nDBCo= +github.com/router-for-me/CLIProxyAPI/v8 v8.0.23 h1:q1bfXpXxPV8BbpSFNJGeqJnReMLyZEW8pAG0y/MHPSQ= +github.com/router-for-me/CLIProxyAPI/v8 v8.0.23/go.mod h1:j1Mtcj0TrC66O1JwbSBB03lY3uYPx4J7yHNvU6nDBCo= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= diff --git a/internal/catalog/codex_client_models.go b/internal/catalog/codex_client_models.go index e5771f681..3c80f2856 100644 --- a/internal/catalog/codex_client_models.go +++ b/internal/catalog/codex_client_models.go @@ -15,7 +15,7 @@ const ( // CodexModelCatalogVersion identifies the Codex client contract represented by the embedded snapshot. CodexModelCatalogVersion = modelcatalog.ClientVersion // CodexModelCatalogCPASDKVersion identifies the CPA SDK release tested with this snapshot. - CodexModelCatalogCPASDKVersion = "v8.0.22" + CodexModelCatalogCPASDKVersion = "v8.0.23" codexModelCatalogSHA256 = modelcatalog.ClientSHA256 codexFallbackModel = "gpt-5.5" ) diff --git a/internal/gateway/websocket_interrupt.go b/internal/gateway/websocket_interrupt.go index 7d4ef6773..3145f312c 100644 --- a/internal/gateway/websocket_interrupt.go +++ b/internal/gateway/websocket_interrupt.go @@ -28,10 +28,14 @@ func (s *websocketConnection) handleInterrupt(turn websocketTurn, fields map[str binding := s.binding parent, found := s.parents[responseID] s.mu.Unlock() - if binding == nil || !found || parent.complete || (turn.lane != "" && turn.lane != parent.lane) { + if binding == nil || !found || (turn.lane != "" && turn.lane != parent.lane) { s.emitReason(turn.lane, reasonWebsocketResponseNotActive) return } + // 已结束响应的迟到中断不应留下 error 帧污染下一轮。 + if parent.complete { + return + } if !websocketGroupEnabled(snapshot, key, binding.ref.GroupID) { s.emitReason(parent.lane, reasonWebsocketDisabled) return @@ -49,11 +53,15 @@ func (s *websocketConnection) handleInterrupt(turn websocketTurn, fields map[str } s.mu.Lock() parent, found = s.parents[responseID] - if !found || parent.complete { + if !found { s.mu.Unlock() s.emitReason(turn.lane, reasonWebsocketResponseNotActive) return } + if parent.complete { + s.mu.Unlock() + return + } // 先登记意图:上游可能在控制帧写入返回前就交付中断终态。 interrupt := &websocketInterrupt{done: make(chan struct{})} parent.interruptRequested = true diff --git a/internal/gateway/websocket_interrupt_test.go b/internal/gateway/websocket_interrupt_test.go index fd512b32f..e15684626 100644 --- a/internal/gateway/websocket_interrupt_test.go +++ b/internal/gateway/websocket_interrupt_test.go @@ -128,10 +128,12 @@ func TestWebsocketInterruptBypassesActiveTurnAndContinues(t *testing.T) { t.Fatal(err) } read("response.incomplete") - if err := conn.WriteMessage(websocket.TextMessage, interrupt); err != nil { - t.Fatal(err) + // 迟到或重复的中断不能留下 error 帧,影响下一轮读取。 + for range 2 { + if err := conn.WriteMessage(websocket.TextMessage, interrupt); err != nil { + t.Fatal(err) + } } - read("error") if err := conn.WriteMessage(websocket.TextMessage, []byte(`{"type":"response.create","model":"public","previous_response_id":"resp_1","input":"answer","store":false}`)); err != nil { t.Fatal(err) } diff --git a/third_party/cpaembedded/embedded/codex_websocket.go b/third_party/cpaembedded/embedded/codex_websocket.go index e4d132409..4d92731f4 100644 --- a/third_party/cpaembedded/embedded/codex_websocket.go +++ b/third_party/cpaembedded/embedded/codex_websocket.go @@ -248,9 +248,7 @@ func (s *CodexWSSession) ExecuteTurn(ctx context.Context, payload json.RawMessag defer s.mu.Unlock() s.activeResponseID = id interrupted := status == "incomplete" && reason == "interrupted" && s.interruptResponseID == id && id != "" - if status == "completed" || status == "incomplete" || status == "failed" || status == "cancelled" { - s.activeResponseID = "" - } + // 保留本轮 ID 到收尾,让 CPA 忽略终态交付期间到达的迟到中断。 return interrupted }, } @@ -359,7 +357,7 @@ func (s *CodexWSSession) ExecuteTurn(ctx context.Context, payload json.RawMessag return result, nil } -// Interrupt 复用 CPA 的控制帧接口,只允许中断当前正在生成的响应。 +// Interrupt 复用 CPA 的控制帧接口,只处理本轮响应;已结束响应的迟到中断由 CPA 忽略。 func (s *CodexWSSession) Interrupt(ctx context.Context, payload []byte) error { if s == nil { return codexWSError("response_not_active") diff --git a/third_party/cpaembedded/embedded/codex_websocket_interrupt_test.go b/third_party/cpaembedded/embedded/codex_websocket_interrupt_test.go index c60f96665..dc1bb8ed8 100644 --- a/third_party/cpaembedded/embedded/codex_websocket_interrupt_test.go +++ b/third_party/cpaembedded/embedded/codex_websocket_interrupt_test.go @@ -225,6 +225,11 @@ func testCodexWSSessionInterruptAndContinue(t *testing.T, eventType string, outp if event.Type == "response.created" { close(created) } + if event.Type == eventType { + if err := sender.Interrupt(t.Context(), interrupt); err != nil { + t.Errorf("interrupt during terminal delivery: %v", err) + } + } return nil }) }() diff --git a/third_party/cpaembedded/go.mod b/third_party/cpaembedded/go.mod index 478f0b287..7b661e7e1 100644 --- a/third_party/cpaembedded/go.mod +++ b/third_party/cpaembedded/go.mod @@ -6,7 +6,7 @@ require ( github.com/andybalholm/brotli v1.2.6 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 - github.com/router-for-me/CLIProxyAPI/v8 v8.0.22 + github.com/router-for-me/CLIProxyAPI/v8 v8.0.23 github.com/sirupsen/logrus v1.10.2 github.com/tidwall/gjson v1.19.0 github.com/tidwall/sjson v1.2.5 diff --git a/third_party/cpaembedded/go.sum b/third_party/cpaembedded/go.sum index 2bcb90a90..e21b4f91e 100644 --- a/third_party/cpaembedded/go.sum +++ b/third_party/cpaembedded/go.sum @@ -70,8 +70,8 @@ github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthO github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= -github.com/router-for-me/CLIProxyAPI/v8 v8.0.22 h1:KX7c+ThdMlhG+SiDSYrMLByAbaqhU4Azl7yR9ihu2HY= -github.com/router-for-me/CLIProxyAPI/v8 v8.0.22/go.mod h1:j1Mtcj0TrC66O1JwbSBB03lY3uYPx4J7yHNvU6nDBCo= +github.com/router-for-me/CLIProxyAPI/v8 v8.0.23 h1:q1bfXpXxPV8BbpSFNJGeqJnReMLyZEW8pAG0y/MHPSQ= +github.com/router-for-me/CLIProxyAPI/v8 v8.0.23/go.mod h1:j1Mtcj0TrC66O1JwbSBB03lY3uYPx4J7yHNvU6nDBCo= github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=