Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.26.3'
go version | grep -q 'go1.26.4'
- name: Unit tests
working-directory: backend
run: make test-unit
Expand Down Expand Up @@ -60,7 +60,7 @@ jobs:
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.26.3'
go version | grep -q 'go1.26.4'
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:

- name: Verify Go version
run: |
go version | grep -q 'go1.26.3'
go version | grep -q 'go1.26.4'

# Docker setup for GoReleaser
- name: Set up QEMU
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
cache-dependency-path: backend/go.sum
- name: Verify Go version
run: |
go version | grep -q 'go1.26.3'
go version | grep -q 'go1.26.4'
- name: Run govulncheck
working-directory: backend
run: |
Expand Down
14 changes: 4 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ frontend/node_modules/
frontend/dist/
*.local
*.tsbuildinfo
vite.config.d.ts
frontend/vite.config.d.ts
vite.config.js.timestamp-*

# 日志
Expand Down Expand Up @@ -136,24 +136,18 @@ backend/.installed
# ===================
# 其他
# ===================
tests
CLAUDE.md
.claude
scripts
.code-review-state
#openspec/
code-reviews/
AGENTS.md
backend/cmd/server/server
deploy/docker-compose.override.yml
.gocache/
vite.config.js
docs/*
!docs/PAYMENT.md
!docs/PAYMENT_CN.md
!docs/ADMIN_PAYMENT_INTEGRATION_API.md
!docs/OFFICIAL_UPDATE_AND_DEPLOY_CN.md
!docs/LOCAL_IMAGE_UPLOAD_DEPLOY_CN.md
frontend/vite.config.js
docs/private/
docs/服务器项目信息.md
.serena/
.codex/
frontend/coverage/
Expand Down
2 changes: 1 addition & 1 deletion backend/cmd/server/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.27
1.0.29
9 changes: 6 additions & 3 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ import (
"github.com/Wei-Shaw/sub2api/internal/web"

"github.com/gin-gonic/gin"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" //nolint:staticcheck // Keep existing h2c behavior until the server moves fully to Go's Protocols API.
)

//go:embed VERSION
Expand Down Expand Up @@ -121,11 +119,16 @@ func runSetupServer() {
log.Printf("Setup wizard available at http://%s", addr)
log.Println("Complete the setup wizard to configure Sub2API")

protocols := new(http.Protocols)
protocols.SetHTTP1(true)
protocols.SetUnencryptedHTTP2(true)

server := &http.Server{
Addr: addr,
Handler: h2c.NewHandler(r, &http2.Server{}), //nolint:staticcheck // Keep existing h2c behavior until the server moves fully to Go's Protocols API.
Handler: r,
ReadHeaderTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
Protocols: protocols,
}

if err := serveServer(server, config.ServerListenSpec{
Expand Down
2 changes: 1 addition & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/Wei-Shaw/sub2api

go 1.26.3
go 1.26.4

require (
entgo.io/ent v0.14.5
Expand Down
19 changes: 13 additions & 6 deletions backend/internal/domain/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,12 @@ var DefaultAntigravityModelMapping = map[string]string{
"claude-haiku-4-5": "claude-sonnet-4-6",
"claude-haiku-4-5-20251001": "claude-sonnet-4-6",
// Gemini 2.5 白名单
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
"gemini-2.5-pro": "gemini-2.5-pro",
"gemini-2.5-flash": "gemini-2.5-flash",
"gemini-2.5-flash-image": "gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview": "gemini-2.5-flash-image",
"gemini-2.5-flash-lite": "gemini-2.5-flash-lite",
"gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking",
"gemini-2.5-pro": "gemini-2.5-pro",
// Gemini 3 白名单
"gemini-3-flash": "gemini-3-flash",
"gemini-3-pro-high": "gemini-3-pro-high",
Expand All @@ -115,10 +117,15 @@ var DefaultAntigravityModelMapping = map[string]string{
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-pro-preview": "gemini-3-pro-high",
// Gemini 3.1 白名单
"gemini-3.1-pro-high": "gemini-3.1-pro-high",
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
"gemini-3.1-flash-image": "gemini-3.1-flash-image",
"gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
"gemini-3.1-pro-high": "gemini-3.1-pro-high",
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
// Gemini 3.1 preview 映射
"gemini-3.1-pro-preview": "gemini-3.1-pro-high",
// Gemini image compatibility aliases
"gemini-3-pro-image": "gemini-3.1-flash-image",
"gemini-3-pro-image-preview": "gemini-3.1-flash-image",
// 其他官方模型
"gpt-oss-120b-medium": "gpt-oss-120b-medium",
"tab_flash_lite_preview": "tab_flash_lite_preview",
Expand Down
26 changes: 15 additions & 11 deletions backend/internal/domain/constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ package domain

import "testing"

func TestDefaultAntigravityModelMapping_ExcludesImageCompatibilityAliases(t *testing.T) {
func TestDefaultAntigravityModelMapping_IncludesImageCompatibilityAliases(t *testing.T) {
t.Parallel()

blocked := []string{
"gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview",
"gemini-3.1-flash-image",
"gemini-3.1-flash-image-preview",
"gemini-3-pro-image",
"gemini-3-pro-image-preview",
expected := map[string]string{
"gemini-2.5-flash-image": "gemini-2.5-flash-image",
"gemini-2.5-flash-image-preview": "gemini-2.5-flash-image",
"gemini-3.1-flash-image": "gemini-3.1-flash-image",
"gemini-3.1-flash-image-preview": "gemini-3.1-flash-image",
"gemini-3-pro-image": "gemini-3.1-flash-image",
"gemini-3-pro-image-preview": "gemini-3.1-flash-image",
}

for _, model := range blocked {
if got, ok := DefaultAntigravityModelMapping[model]; ok {
t.Fatalf("did not expect image generation model %q in default mapping, got %q", model, got)
for model, want := range expected {
got, ok := DefaultAntigravityModelMapping[model]
if !ok {
t.Fatalf("expected image generation model %q in default mapping", model)
}
if got != want {
t.Fatalf("DefaultAntigravityModelMapping[%q] = %q, want %q", model, got, want)
}
}
}
12 changes: 12 additions & 0 deletions backend/internal/handler/admin/setting_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
EnableFingerprintUnification: settings.EnableFingerprintUnification,
EnableMetadataPassthrough: settings.EnableMetadataPassthrough,
EnableCCHSigning: settings.EnableCCHSigning,
OpenAICleanRelayEnabled: settings.OpenAICleanRelayEnabled,
EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
PaymentVisibleMethodAlipaySource: settings.PaymentVisibleMethodAlipaySource,
Expand Down Expand Up @@ -575,6 +576,7 @@ type UpdateSettingsRequest struct {
EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
EnableCCHSigning *bool `json:"enable_cch_signing"`
OpenAICleanRelayEnabled *bool `json:"openai_clean_relay_enabled"`
EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`

// Payment visible method routing
Expand Down Expand Up @@ -1547,6 +1549,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.EnableCCHSigning
}(),
OpenAICleanRelayEnabled: func() bool {
if req.OpenAICleanRelayEnabled != nil {
return *req.OpenAICleanRelayEnabled
}
return previousSettings.OpenAICleanRelayEnabled
}(),
EnableAnthropicCacheTTL1hInjection: func() bool {
if req.EnableAnthropicCacheTTL1hInjection != nil {
return *req.EnableAnthropicCacheTTL1hInjection
Expand Down Expand Up @@ -1915,6 +1923,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification,
EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
EnableCCHSigning: updatedSettings.EnableCCHSigning,
OpenAICleanRelayEnabled: updatedSettings.OpenAICleanRelayEnabled,
EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
PaymentVisibleMethodWxpaySource: updatedSettings.PaymentVisibleMethodWxpaySource,
Expand Down Expand Up @@ -2680,6 +2689,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.EnableCCHSigning != after.EnableCCHSigning {
changed = append(changed, "enable_cch_signing")
}
if before.OpenAICleanRelayEnabled != after.OpenAICleanRelayEnabled {
changed = append(changed, "openai_clean_relay_enabled")
}
if before.EnableAnthropicCacheTTL1hInjection != after.EnableAnthropicCacheTTL1hInjection {
changed = append(changed, "enable_anthropic_cache_ttl_1h_injection")
}
Expand Down
39 changes: 39 additions & 0 deletions backend/internal/handler/dto/credentials_redact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dto

import "github.com/Wei-Shaw/sub2api/internal/service"

// RedactCredentials returns a copy of credentials without sensitive keys plus
// has_<key> status flags so the UI can tell configured secrets from missing ones.
func RedactCredentials(in map[string]any) (map[string]any, map[string]bool) {
if in == nil {
return nil, nil
}
out := make(map[string]any, len(in))
var status map[string]bool
for key, value := range in {
if service.IsSensitiveCredentialKey(key) {
if isCredentialValuePresent(value) {
if status == nil {
status = make(map[string]bool, 4)
}
status["has_"+key] = true
}
continue
}
out[key] = value
}
return out, status
}

func isCredentialValuePresent(value any) bool {
switch v := value.(type) {
case nil:
return false
case string:
return v != ""
case bool:
return v
default:
return true
}
}
4 changes: 3 additions & 1 deletion backend/internal/handler/dto/mappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,16 @@ func AccountFromServiceShallow(a *service.Account) *Account {
if a == nil {
return nil
}
redactedCredentials, credentialsStatus := RedactCredentials(a.Credentials)
out := &Account{
ID: a.ID,
Name: a.Name,
Notes: a.Notes,
Platform: a.Platform,
AccountLevel: service.NormalizeAccountLevel(a.AccountLevel),
Type: a.Type,
Credentials: a.Credentials,
Credentials: redactedCredentials,
CredentialsStatus: credentialsStatus,
Extra: a.Extra,
OwnerUserID: a.OwnerUserID,
ShareMode: service.NormalizeAccountShareMode(a.ShareMode),
Expand Down
1 change: 1 addition & 0 deletions backend/internal/handler/dto/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ type SystemSettings struct {
EnableFingerprintUnification bool `json:"enable_fingerprint_unification"`
EnableMetadataPassthrough bool `json:"enable_metadata_passthrough"`
EnableCCHSigning bool `json:"enable_cch_signing"`
OpenAICleanRelayEnabled bool `json:"openai_clean_relay_enabled"`
EnableAnthropicCacheTTL1hInjection bool `json:"enable_anthropic_cache_ttl_1h_injection"`

// Web Search Emulation
Expand Down
49 changes: 25 additions & 24 deletions backend/internal/handler/dto/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,30 +168,31 @@ type AdminGroup struct {
}

type Account struct {
ID int64 `json:"id"`
Name string `json:"name"`
Notes *string `json:"notes"`
Platform string `json:"platform"`
AccountLevel string `json:"account_level"`
Type string `json:"type"`
Credentials map[string]any `json:"credentials"`
Extra map[string]any `json:"extra"`
OwnerUserID *int64 `json:"owner_user_id,omitempty"`
ShareMode string `json:"share_mode"`
ShareStatus string `json:"share_status"`
SharePolicyID *int64 `json:"share_policy_id,omitempty"`
ProxyID *int64 `json:"proxy_id"`
Concurrency int `json:"concurrency"`
LoadFactor *int `json:"load_factor,omitempty"`
Priority int `json:"priority"`
RateMultiplier float64 `json:"rate_multiplier"`
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
LastUsedAt *time.Time `json:"last_used_at"`
ExpiresAt *int64 `json:"expires_at"`
AutoPauseOnExpired bool `json:"auto_pause_on_expired"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID int64 `json:"id"`
Name string `json:"name"`
Notes *string `json:"notes"`
Platform string `json:"platform"`
AccountLevel string `json:"account_level"`
Type string `json:"type"`
Credentials map[string]any `json:"credentials"`
CredentialsStatus map[string]bool `json:"credentials_status,omitempty"`
Extra map[string]any `json:"extra"`
OwnerUserID *int64 `json:"owner_user_id,omitempty"`
ShareMode string `json:"share_mode"`
ShareStatus string `json:"share_status"`
SharePolicyID *int64 `json:"share_policy_id,omitempty"`
ProxyID *int64 `json:"proxy_id"`
Concurrency int `json:"concurrency"`
LoadFactor *int `json:"load_factor,omitempty"`
Priority int `json:"priority"`
RateMultiplier float64 `json:"rate_multiplier"`
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
LastUsedAt *time.Time `json:"last_used_at"`
ExpiresAt *int64 `json:"expires_at"`
AutoPauseOnExpired bool `json:"auto_pause_on_expired"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`

Schedulable bool `json:"schedulable"`

Expand Down
11 changes: 9 additions & 2 deletions backend/internal/handler/openai_chat_completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,19 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) {
zap.Int("excluded_account_count", len(failedAccountIDs)),
zap.Int64p("group_id", currentAPIKey.GroupID),
)
selection, scheduleDecision, err := h.gatewayService.SelectAccountWithScheduler(
selectionModel := resolveOpenAIAccountSelectionModel(reqModel, channelMapping)
selection, scheduleDecision, err := h.gatewayService.SelectAccountWithCleanRelayScheduler(
c.Request.Context(),
c,
currentAPIKey.GroupID,
"",
sessionHash,
reqModel,
selectionModel,
failedAccountIDs,
service.OpenAIUpstreamTransportAny,
false,
body,
)
if err != nil {
reqLog.Warn("openai_chat_completions.account_select_failed",
Expand All @@ -172,15 +176,18 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) {
reqLog.Info("openai_chat_completions.fallback_to_default_model",
zap.String("default_mapped_model", defaultModel),
)
selection, scheduleDecision, err = h.gatewayService.SelectAccountWithScheduler(
selection, scheduleDecision, err = h.gatewayService.SelectAccountWithCleanRelayScheduler(
c.Request.Context(),
c,
currentAPIKey.GroupID,
"",
sessionHash,
defaultModel,
defaultModel,
failedAccountIDs,
service.OpenAIUpstreamTransportAny,
false,
body,
)
if err == nil && selection != nil {
c.Set("openai_chat_completions_fallback_model", defaultModel)
Expand Down
Loading
Loading