Skip to content

Commit eaff70e

Browse files
committed
Merge remote-tracking branch 'upstream/main' into sync/upstream-main-20260529
2 parents 8cd0a57 + 7321e4d commit eaff70e

113 files changed

Lines changed: 9638 additions & 2842 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/cmd/server/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.132
1+
0.1.133

backend/cmd/server/wire_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/internal/domain/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const (
7272
// 与前端 useModelWhitelist.ts 中的 antigravityDefaultMappings 保持一致
7373
var DefaultAntigravityModelMapping = map[string]string{
7474
// Claude 白名单
75+
"claude-opus-4-8": "claude-opus-4-8", // 官方模型
7576
"claude-opus-4-7": "claude-opus-4-7", // 官方模型
7677
"claude-opus-4-6-thinking": "claude-opus-4-6-thinking", // 官方模型
7778
"claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射
@@ -122,6 +123,7 @@ var DefaultAntigravityModelMapping = map[string]string{
122123
// aws_region 自动调整为匹配的区域前缀(如 eu.、apac.、jp. 等)
123124
var DefaultBedrockModelMapping = map[string]string{
124125
// Claude Opus
126+
"claude-opus-4-8": "us.anthropic.claude-opus-4-8-v1",
125127
"claude-opus-4-7": "us.anthropic.claude-opus-4-7-v1",
126128
"claude-opus-4-6-thinking": "us.anthropic.claude-opus-4-6-v1",
127129
"claude-opus-4-6": "us.anthropic.claude-opus-4-6-v1",

backend/internal/domain/constants_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,27 @@ func TestDefaultAntigravityModelMapping_ImageCompatibilityAliases(t *testing.T)
2424
}
2525
}
2626
}
27+
28+
func TestDefaultAntigravityModelMapping_ContainsOpus48(t *testing.T) {
29+
t.Parallel()
30+
31+
got, ok := DefaultAntigravityModelMapping["claude-opus-4-8"]
32+
if !ok {
33+
t.Fatal("expected mapping for claude-opus-4-8 to exist")
34+
}
35+
if got != "claude-opus-4-8" {
36+
t.Fatalf("unexpected claude-opus-4-8 mapping: got %q", got)
37+
}
38+
}
39+
40+
func TestDefaultBedrockModelMapping_ContainsOpus48(t *testing.T) {
41+
t.Parallel()
42+
43+
got, ok := DefaultBedrockModelMapping["claude-opus-4-8"]
44+
if !ok {
45+
t.Fatal("expected Bedrock mapping for claude-opus-4-8 to exist")
46+
}
47+
if got != "us.anthropic.claude-opus-4-8-v1" {
48+
t.Fatalf("unexpected Bedrock claude-opus-4-8 mapping: got %q", got)
49+
}
50+
}

backend/internal/handler/admin/setting_handler.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
256256
RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
257257
AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
258258
OpenAICodexUserAgent: settings.OpenAICodexUserAgent,
259+
OpenAIAllowClaudeCodeCodexPlugin: settings.OpenAIAllowClaudeCodeCodexPlugin,
259260
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
260261
PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
261262
PaymentVisibleMethodWxpaySource: settings.PaymentVisibleMethodWxpaySource,
@@ -584,6 +585,7 @@ type UpdateSettingsRequest struct {
584585
RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
585586
AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
586587
OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
588+
OpenAIAllowClaudeCodeCodexPlugin *bool `json:"openai_allow_claude_code_codex_plugin"`
587589

588590
// Payment visible method routing
589591
PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
@@ -1655,6 +1657,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
16551657
}
16561658
return previousSettings.OpenAICodexUserAgent
16571659
}(),
1660+
OpenAIAllowClaudeCodeCodexPlugin: func() bool {
1661+
if req.OpenAIAllowClaudeCodeCodexPlugin != nil {
1662+
return *req.OpenAIAllowClaudeCodeCodexPlugin
1663+
}
1664+
return previousSettings.OpenAIAllowClaudeCodeCodexPlugin
1665+
}(),
16581666
PaymentVisibleMethodAlipaySource: func() string {
16591667
if req.PaymentVisibleMethodAlipaySource != nil {
16601668
return strings.TrimSpace(*req.PaymentVisibleMethodAlipaySource)
@@ -2031,6 +2039,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
20312039
RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
20322040
AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
20332041
OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
2042+
OpenAIAllowClaudeCodeCodexPlugin: updatedSettings.OpenAIAllowClaudeCodeCodexPlugin,
20342043
PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
20352044
PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
20362045
PaymentVisibleMethodAlipayEnabled: updatedSettings.PaymentVisibleMethodAlipayEnabled,
@@ -2500,6 +2509,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
25002509
if before.OpenAICodexUserAgent != after.OpenAICodexUserAgent {
25012510
changed = append(changed, "openai_codex_user_agent")
25022511
}
2512+
if before.OpenAIAllowClaudeCodeCodexPlugin != after.OpenAIAllowClaudeCodeCodexPlugin {
2513+
changed = append(changed, "openai_allow_claude_code_codex_plugin")
2514+
}
25032515
if before.PaymentVisibleMethodAlipaySource != after.PaymentVisibleMethodAlipaySource {
25042516
changed = append(changed, "payment_visible_method_alipay_source")
25052517
}

backend/internal/handler/admin/system_handler.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package admin
22

33
import (
44
"context"
5+
"errors"
56
"net/http"
67
"strconv"
78
"strings"
@@ -17,12 +18,18 @@ import (
1718

1819
// SystemHandler handles system-related operations
1920
type SystemHandler struct {
20-
updateSvc *service.UpdateService
21+
updateSvc systemUpdateService
2122
lockSvc *service.SystemOperationLockService
2223
}
2324

25+
type systemUpdateService interface {
26+
CheckUpdate(ctx context.Context, force bool) (*service.UpdateInfo, error)
27+
PerformUpdate(ctx context.Context) error
28+
Rollback() error
29+
}
30+
2431
// NewSystemHandler creates a new SystemHandler
25-
func NewSystemHandler(updateSvc *service.UpdateService, lockSvc *service.SystemOperationLockService) *SystemHandler {
32+
func NewSystemHandler(updateSvc systemUpdateService, lockSvc *service.SystemOperationLockService) *SystemHandler {
2633
return &SystemHandler{
2734
updateSvc: updateSvc,
2835
lockSvc: lockSvc,
@@ -67,6 +74,21 @@ func (h *SystemHandler) PerformUpdate(c *gin.Context) {
6774
}()
6875

6976
if err := h.updateSvc.PerformUpdate(ctx); err != nil {
77+
if errors.Is(err, service.ErrNoUpdateAvailable) {
78+
info, checkErr := h.updateSvc.CheckUpdate(ctx, false)
79+
if checkErr != nil {
80+
releaseReason = "SYSTEM_UPDATE_FAILED"
81+
return nil, checkErr
82+
}
83+
succeeded = true
84+
return gin.H{
85+
"message": "Already up to date",
86+
"already_up_to_date": true,
87+
"current_version": info.CurrentVersion,
88+
"latest_version": info.LatestVersion,
89+
"operation_id": lock.OperationID(),
90+
}, nil
91+
}
7092
releaseReason = "SYSTEM_UPDATE_FAILED"
7193
return nil, err
7294
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//go:build unit
2+
3+
package admin
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"errors"
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
"time"
13+
14+
"github.com/Wei-Shaw/sub2api/internal/service"
15+
"github.com/gin-gonic/gin"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
type systemHandlerUpdateServiceStub struct {
20+
performErr error
21+
updateInfo *service.UpdateInfo
22+
checkErr error
23+
checkForces []bool
24+
performCall int
25+
}
26+
27+
func (s *systemHandlerUpdateServiceStub) CheckUpdate(_ context.Context, force bool) (*service.UpdateInfo, error) {
28+
s.checkForces = append(s.checkForces, force)
29+
return s.updateInfo, s.checkErr
30+
}
31+
32+
func (s *systemHandlerUpdateServiceStub) PerformUpdate(context.Context) error {
33+
s.performCall++
34+
return s.performErr
35+
}
36+
37+
func (s *systemHandlerUpdateServiceStub) Rollback() error {
38+
return nil
39+
}
40+
41+
type systemUpdateResponseEnvelope struct {
42+
Code int `json:"code"`
43+
Message string `json:"message"`
44+
Data struct {
45+
Message string `json:"message"`
46+
AlreadyUpToDate bool `json:"already_up_to_date"`
47+
CurrentVersion string `json:"current_version"`
48+
LatestVersion string `json:"latest_version"`
49+
OperationID string `json:"operation_id"`
50+
} `json:"data"`
51+
}
52+
53+
type systemUpdateErrorEnvelope struct {
54+
Code int `json:"code"`
55+
Message string `json:"message"`
56+
}
57+
58+
func newSystemHandlerTestRouter(t *testing.T, updateSvc *systemHandlerUpdateServiceStub, repo *memoryIdempotencyRepoStub) *gin.Engine {
59+
t.Helper()
60+
gin.SetMode(gin.TestMode)
61+
service.SetDefaultIdempotencyCoordinator(nil)
62+
t.Cleanup(func() {
63+
service.SetDefaultIdempotencyCoordinator(nil)
64+
})
65+
66+
lockSvc := service.NewSystemOperationLockService(repo, service.IdempotencyConfig{
67+
ProcessingTimeout: time.Second,
68+
SystemOperationTTL: time.Minute,
69+
})
70+
handler := NewSystemHandler(updateSvc, lockSvc)
71+
72+
router := gin.New()
73+
router.POST("/api/v1/admin/system/update", handler.PerformUpdate)
74+
return router
75+
}
76+
77+
func requireSystemLockStatus(t *testing.T, repo *memoryIdempotencyRepoStub, wantStatus string) {
78+
t.Helper()
79+
repo.mu.Lock()
80+
defer repo.mu.Unlock()
81+
82+
for _, record := range repo.data {
83+
if record.Status == wantStatus {
84+
return
85+
}
86+
}
87+
t.Fatalf("system lock status %q not found in records: %#v", wantStatus, repo.data)
88+
}
89+
90+
func TestSystemHandlerPerformUpdateAlreadyUpToDateReturnsOK(t *testing.T) {
91+
updateSvc := &systemHandlerUpdateServiceStub{
92+
performErr: service.ErrNoUpdateAvailable,
93+
updateInfo: &service.UpdateInfo{
94+
CurrentVersion: "0.1.132",
95+
LatestVersion: "0.1.132",
96+
HasUpdate: false,
97+
},
98+
}
99+
repo := newMemoryIdempotencyRepoStub()
100+
router := newSystemHandlerTestRouter(t, updateSvc, repo)
101+
102+
rec := httptest.NewRecorder()
103+
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/system/update", nil)
104+
req.Header.Set("Idempotency-Key", "already-up-to-date")
105+
router.ServeHTTP(rec, req)
106+
107+
require.Equal(t, http.StatusOK, rec.Code)
108+
require.Equal(t, 1, updateSvc.performCall)
109+
require.Equal(t, []bool{false}, updateSvc.checkForces)
110+
requireSystemLockStatus(t, repo, service.IdempotencyStatusSucceeded)
111+
112+
var body systemUpdateResponseEnvelope
113+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
114+
require.Equal(t, 0, body.Code)
115+
require.Equal(t, "success", body.Message)
116+
require.Equal(t, "Already up to date", body.Data.Message)
117+
require.True(t, body.Data.AlreadyUpToDate)
118+
require.Equal(t, "0.1.132", body.Data.CurrentVersion)
119+
require.Equal(t, "0.1.132", body.Data.LatestVersion)
120+
require.NotEmpty(t, body.Data.OperationID)
121+
}
122+
123+
func TestSystemHandlerPerformUpdateFailureStillReturnsInternalError(t *testing.T) {
124+
updateSvc := &systemHandlerUpdateServiceStub{
125+
performErr: errors.New("download failed"),
126+
}
127+
repo := newMemoryIdempotencyRepoStub()
128+
router := newSystemHandlerTestRouter(t, updateSvc, repo)
129+
130+
rec := httptest.NewRecorder()
131+
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/system/update", nil)
132+
req.Header.Set("Idempotency-Key", "real-failure")
133+
router.ServeHTTP(rec, req)
134+
135+
require.Equal(t, http.StatusInternalServerError, rec.Code)
136+
require.Equal(t, 1, updateSvc.performCall)
137+
require.Empty(t, updateSvc.checkForces)
138+
requireSystemLockStatus(t, repo, service.IdempotencyStatusFailedRetryable)
139+
140+
var body systemUpdateErrorEnvelope
141+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
142+
require.Equal(t, http.StatusInternalServerError, body.Code)
143+
require.Equal(t, "internal error", body.Message)
144+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
)
9+
10+
const statusClientClosedRequest = 499
11+
12+
func concurrencyErrorResponse(err error, slotType string) (int, string, string) {
13+
var concurrencyErr *ConcurrencyError
14+
if errors.As(err, &concurrencyErr) {
15+
if concurrencyErr.SlotType != "" {
16+
slotType = concurrencyErr.SlotType
17+
}
18+
return http.StatusTooManyRequests, "rate_limit_error",
19+
fmt.Sprintf("Concurrency limit exceeded for %s, please retry later", slotType)
20+
}
21+
22+
if errors.Is(err, context.Canceled) {
23+
return statusClientClosedRequest, "api_error", "context canceled"
24+
}
25+
26+
return http.StatusServiceUnavailable, "api_error", "Service temporarily unavailable, please retry later"
27+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestConcurrencyErrorResponse(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
err error
16+
slotType string
17+
wantStatus int
18+
wantType string
19+
wantMessage string
20+
}{
21+
{
22+
name: "true concurrency timeout remains rate limit",
23+
err: &ConcurrencyError{SlotType: "account", IsTimeout: true},
24+
slotType: "user",
25+
wantStatus: http.StatusTooManyRequests,
26+
wantType: "rate_limit_error",
27+
wantMessage: "Concurrency limit exceeded for account, please retry later",
28+
},
29+
{
30+
name: "client cancellation is not classified as concurrency limit",
31+
err: context.Canceled,
32+
slotType: "user",
33+
wantStatus: statusClientClosedRequest,
34+
wantType: "api_error",
35+
wantMessage: "context canceled",
36+
},
37+
{
38+
name: "deadline exceeded is service unavailable",
39+
err: context.DeadlineExceeded,
40+
slotType: "user",
41+
wantStatus: http.StatusServiceUnavailable,
42+
wantType: "api_error",
43+
wantMessage: "Service temporarily unavailable, please retry later",
44+
},
45+
{
46+
name: "redis acquire error is service unavailable",
47+
err: errors.New("redis unavailable"),
48+
slotType: "user",
49+
wantStatus: http.StatusServiceUnavailable,
50+
wantType: "api_error",
51+
wantMessage: "Service temporarily unavailable, please retry later",
52+
},
53+
}
54+
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) {
57+
status, errType, message := concurrencyErrorResponse(tt.err, tt.slotType)
58+
require.Equal(t, tt.wantStatus, status)
59+
require.Equal(t, tt.wantType, errType)
60+
require.Equal(t, tt.wantMessage, message)
61+
})
62+
}
63+
}

backend/internal/handler/dto/settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ type SystemSettings struct {
185185
RewriteMessageCacheControl bool `json:"rewrite_message_cache_control"`
186186
AntigravityUserAgentVersion string `json:"antigravity_user_agent_version"`
187187
OpenAICodexUserAgent string `json:"openai_codex_user_agent"`
188+
OpenAIAllowClaudeCodeCodexPlugin bool `json:"openai_allow_claude_code_codex_plugin"`
188189

189190
// Web Search Emulation
190191
WebSearchEmulationEnabled bool `json:"web_search_emulation_enabled"`

0 commit comments

Comments
 (0)