Skip to content
Open
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
2 changes: 1 addition & 1 deletion backend/cmd/server/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,34 @@ const (
ImageConcurrencyOverflowModeWait = "wait"
)

const (
OpenAIImagesResponsesReasoningEffortLow = "low"
OpenAIImagesResponsesReasoningEffortMedium = "medium"
OpenAIImagesResponsesReasoningEffortHigh = "high"
OpenAIImagesResponsesReasoningEffortXHigh = "xhigh"
OpenAIImagesResponsesReasoningEffortDefault = OpenAIImagesResponsesReasoningEffortMedium
)

func IsValidOpenAIImagesResponsesReasoningEffort(raw string) bool {
switch strings.ToLower(strings.TrimSpace(raw)) {
case OpenAIImagesResponsesReasoningEffortLow,
OpenAIImagesResponsesReasoningEffortMedium,
OpenAIImagesResponsesReasoningEffortHigh,
OpenAIImagesResponsesReasoningEffortXHigh:
return true
default:
return false
}
}

func NormalizeOpenAIImagesResponsesReasoningEffort(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw))
if IsValidOpenAIImagesResponsesReasoningEffort(normalized) {
return normalized
}
return OpenAIImagesResponsesReasoningEffortDefault
}

// GatewayConfig API网关相关配置
type GatewayConfig struct {
// 等待上游响应头的超时时间(秒),0表示无超时
Expand All @@ -705,6 +733,9 @@ type GatewayConfig struct {
// CodexImageGenerationBridgeEnabled: 是否为 Codex `/v1/responses` 自动注入 image_generation 工具和桥接指令。
// 默认关闭,避免纯文本 Codex 请求被意外改写;显式携带 image_generation 工具的请求仍按分组能力转发。
CodexImageGenerationBridgeEnabled bool `mapstructure:"codex_image_generation_bridge_enabled"`
// OpenAIImagesResponsesReasoningEffort: OAuth 图片桥接走 Responses API 时的 reasoning.effort。
// 允许 low/medium/high/xhigh,默认 medium。
OpenAIImagesResponsesReasoningEffort string `mapstructure:"openai_images_responses_reasoning_effort"`
// ForcedCodexInstructionsTemplateFile: 服务端强制附加到 Codex 顶层 instructions 的模板文件路径。
// 模板渲染后会直接覆盖最终 instructions;若需要保留客户端 system 转换结果,请在模板中显式引用 {{ .ExistingInstructions }}。
ForcedCodexInstructionsTemplateFile string `mapstructure:"forced_codex_instructions_template_file"`
Expand Down Expand Up @@ -1422,6 +1453,10 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
cfg.Log.Environment = strings.TrimSpace(cfg.Log.Environment)
cfg.Log.StacktraceLevel = strings.ToLower(strings.TrimSpace(cfg.Log.StacktraceLevel))
cfg.Log.Output.FilePath = strings.TrimSpace(cfg.Log.Output.FilePath)
cfg.Gateway.OpenAIImagesResponsesReasoningEffort = strings.ToLower(strings.TrimSpace(cfg.Gateway.OpenAIImagesResponsesReasoningEffort))
if cfg.Gateway.OpenAIImagesResponsesReasoningEffort == "" {
cfg.Gateway.OpenAIImagesResponsesReasoningEffort = OpenAIImagesResponsesReasoningEffortDefault
}
cfg.Gateway.ForcedCodexInstructionsTemplateFile = strings.TrimSpace(cfg.Gateway.ForcedCodexInstructionsTemplateFile)
if cfg.Gateway.ForcedCodexInstructionsTemplateFile != "" {
content, err := os.ReadFile(cfg.Gateway.ForcedCodexInstructionsTemplateFile)
Expand Down Expand Up @@ -1779,6 +1814,7 @@ func setDefaults() {
viper.SetDefault("gateway.max_account_switches_gemini", 3)
viper.SetDefault("gateway.force_codex_cli", false)
viper.SetDefault("gateway.codex_image_generation_bridge_enabled", false)
viper.SetDefault("gateway.openai_images_responses_reasoning_effort", OpenAIImagesResponsesReasoningEffortDefault)
viper.SetDefault("gateway.openai_passthrough_allow_timeout_headers", false)
// OpenAI Responses WebSocket(默认开启;可通过 force_http 紧急回滚)
viper.SetDefault("gateway.openai_ws.enabled", true)
Expand Down Expand Up @@ -2428,6 +2464,9 @@ func (c *Config) Validate() error {
if c.Gateway.ImageConcurrency.MaxWaitingRequests < 0 {
return fmt.Errorf("gateway.image_concurrency.max_waiting_requests must be non-negative")
}
if !IsValidOpenAIImagesResponsesReasoningEffort(c.Gateway.OpenAIImagesResponsesReasoningEffort) {
return fmt.Errorf("gateway.openai_images_responses_reasoning_effort must be one of: low/medium/high/xhigh")
}
if c.Gateway.MaxIdleConns <= 0 {
return fmt.Errorf("gateway.max_idle_conns must be positive")
}
Expand Down
8 changes: 8 additions & 0 deletions backend/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ func TestLoadDefaultOpenAIWSConfig(t *testing.T) {
if cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor != 1.0 {
t.Fatalf("Gateway.OpenAIWS.APIKeyMaxConnsFactor = %v, want 1.0", cfg.Gateway.OpenAIWS.APIKeyMaxConnsFactor)
}
if cfg.Gateway.OpenAIImagesResponsesReasoningEffort != "medium" {
t.Fatalf("Gateway.OpenAIImagesResponsesReasoningEffort = %q, want medium", cfg.Gateway.OpenAIImagesResponsesReasoningEffort)
}
if cfg.Gateway.OpenAIWS.StickySessionTTLSeconds != 3600 {
t.Fatalf("Gateway.OpenAIWS.StickySessionTTLSeconds = %d, want 3600", cfg.Gateway.OpenAIWS.StickySessionTTLSeconds)
}
Expand Down Expand Up @@ -1385,6 +1388,11 @@ func TestValidateConfigErrors(t *testing.T) {
mutate: func(c *Config) { c.Gateway.ImageConcurrency.MaxWaitingRequests = -1 },
wantErr: "gateway.image_concurrency.max_waiting_requests must be non-negative",
},
{
name: "gateway openai images responses reasoning effort invalid",
mutate: func(c *Config) { c.Gateway.OpenAIImagesResponsesReasoningEffort = "advanced" },
wantErr: "gateway.openai_images_responses_reasoning_effort must be one of: low/medium/high/xhigh",
},
{
name: "gateway max line size",
mutate: func(c *Config) { c.Gateway.MaxLineSize = 1024 },
Expand Down
70 changes: 70 additions & 0 deletions backend/internal/handler/admin/account_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,20 @@ type TestAccountRequest struct {
Mode string `json:"mode"`
}

type ModelProbeListRequest struct {
Platform string `json:"platform" binding:"required"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key" binding:"required"`
}

type ModelProbeTestRequest struct {
Platform string `json:"platform" binding:"required"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key" binding:"required"`
Mode string `json:"mode"`
Models []string `json:"models" binding:"required"`
}

type SyncFromCRSRequest struct {
BaseURL string `json:"base_url" binding:"required"`
Username string `json:"username" binding:"required"`
Expand Down Expand Up @@ -743,6 +757,62 @@ func (h *AccountHandler) Test(c *gin.Context) {
}
}

// ProbeModelList discovers upstream models with a temporary URL and API key.
// POST /api/v1/admin/accounts/model-probe/list
func (h *AccountHandler) ProbeModelList(c *gin.Context) {
if h.accountTestService == nil {
response.Error(c, http.StatusServiceUnavailable, "Account test service unavailable")
return
}

var req ModelProbeListRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}

result, err := h.accountTestService.ProbeModelList(c.Request.Context(), service.ModelProbeListInput{
Platform: req.Platform,
BaseURL: req.BaseURL,
APIKey: req.APIKey,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}

response.Success(c, result)
}

// ProbeModels sends minimal requests to selected upstream models.
// POST /api/v1/admin/accounts/model-probe/test
func (h *AccountHandler) ProbeModels(c *gin.Context) {
if h.accountTestService == nil {
response.Error(c, http.StatusServiceUnavailable, "Account test service unavailable")
return
}

var req ModelProbeTestRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}

result, err := h.accountTestService.ProbeModels(c.Request.Context(), service.ModelProbeTestInput{
Platform: req.Platform,
BaseURL: req.BaseURL,
APIKey: req.APIKey,
Mode: req.Mode,
Models: req.Models,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}

response.Success(c, result)
}

// RecoverState handles unified recovery of recoverable account runtime state.
// POST /api/v1/admin/accounts/:id/recover-state
func (h *AccountHandler) RecoverState(c *gin.Context) {
Expand Down
36 changes: 28 additions & 8 deletions backend/internal/handler/admin/setting_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
EnableAnthropicCacheTTL1hInjection: settings.EnableAnthropicCacheTTL1hInjection,
RewriteMessageCacheControl: settings.RewriteMessageCacheControl,
AntigravityUserAgentVersion: settings.AntigravityUserAgentVersion,
OpenAIImagesResponsesReasoningEffort: settings.OpenAIImagesResponsesReasoningEffort,
OpenAICodexUserAgent: settings.OpenAICodexUserAgent,
OpenAIAllowClaudeCodeCodexPlugin: settings.OpenAIAllowClaudeCodeCodexPlugin,
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
Expand Down Expand Up @@ -578,14 +579,15 @@ type UpdateSettingsRequest struct {
BackendModeEnabled bool `json:"backend_mode_enabled"`

// Gateway forwarding behavior
EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
EnableCCHSigning *bool `json:"enable_cch_signing"`
EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
OpenAIAllowClaudeCodeCodexPlugin *bool `json:"openai_allow_claude_code_codex_plugin"`
EnableFingerprintUnification *bool `json:"enable_fingerprint_unification"`
EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"`
EnableCCHSigning *bool `json:"enable_cch_signing"`
EnableAnthropicCacheTTL1hInjection *bool `json:"enable_anthropic_cache_ttl_1h_injection"`
RewriteMessageCacheControl *bool `json:"rewrite_message_cache_control"`
AntigravityUserAgentVersion *string `json:"antigravity_user_agent_version"`
OpenAIImagesResponsesReasoningEffort *string `json:"openai_images_responses_reasoning_effort"`
OpenAICodexUserAgent *string `json:"openai_codex_user_agent"`
OpenAIAllowClaudeCodeCodexPlugin *bool `json:"openai_allow_claude_code_codex_plugin"`

// Payment visible method routing
PaymentVisibleMethodAlipaySource *string `json:"payment_visible_method_alipay_source"`
Expand Down Expand Up @@ -1440,6 +1442,14 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
return
}
}
if req.OpenAIImagesResponsesReasoningEffort != nil {
normalized := strings.ToLower(strings.TrimSpace(*req.OpenAIImagesResponsesReasoningEffort))
req.OpenAIImagesResponsesReasoningEffort = &normalized
if normalized != "" && !service.IsValidOpenAIImagesResponsesReasoningEffort(normalized) {
response.Error(c, http.StatusBadRequest, "openai_images_responses_reasoning_effort must be one of: low, medium, high, xhigh")
return
}
}
if req.OpenAICodexUserAgent != nil {
normalized := strings.TrimSpace(*req.OpenAICodexUserAgent)
req.OpenAICodexUserAgent = &normalized
Expand Down Expand Up @@ -1651,6 +1661,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
}
return previousSettings.AntigravityUserAgentVersion
}(),
OpenAIImagesResponsesReasoningEffort: func() string {
if req.OpenAIImagesResponsesReasoningEffort != nil {
return *req.OpenAIImagesResponsesReasoningEffort
}
return previousSettings.OpenAIImagesResponsesReasoningEffort
}(),
OpenAICodexUserAgent: func() string {
if req.OpenAICodexUserAgent != nil {
return *req.OpenAICodexUserAgent
Expand Down Expand Up @@ -2038,6 +2054,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
EnableAnthropicCacheTTL1hInjection: updatedSettings.EnableAnthropicCacheTTL1hInjection,
RewriteMessageCacheControl: updatedSettings.RewriteMessageCacheControl,
AntigravityUserAgentVersion: updatedSettings.AntigravityUserAgentVersion,
OpenAIImagesResponsesReasoningEffort: updatedSettings.OpenAIImagesResponsesReasoningEffort,
OpenAICodexUserAgent: updatedSettings.OpenAICodexUserAgent,
OpenAIAllowClaudeCodeCodexPlugin: updatedSettings.OpenAIAllowClaudeCodeCodexPlugin,
PaymentVisibleMethodAlipaySource: updatedSettings.PaymentVisibleMethodAlipaySource,
Expand Down Expand Up @@ -2506,6 +2523,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.AntigravityUserAgentVersion != after.AntigravityUserAgentVersion {
changed = append(changed, "antigravity_user_agent_version")
}
if before.OpenAIImagesResponsesReasoningEffort != after.OpenAIImagesResponsesReasoningEffort {
changed = append(changed, "openai_images_responses_reasoning_effort")
}
if before.OpenAICodexUserAgent != after.OpenAICodexUserAgent {
changed = append(changed, "openai_codex_user_agent")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,68 @@ func TestSettingHandler_UpdateSettings_PersistsPaymentVisibleMethodsAndAdvancedS
require.Equal(t, true, data["openai_advanced_scheduler_enabled"])
}

func TestSettingHandler_UpdateSettings_PersistsOpenAIImagesResponsesReasoningEffort(t *testing.T) {
gin.SetMode(gin.TestMode)
repo := &settingHandlerRepoStub{
values: map[string]string{
service.SettingKeyPromoCodeEnabled: "true",
},
}
svc := service.NewSettingService(repo, &config.Config{Default: config.DefaultConfig{UserConcurrency: 5}})
handler := NewSettingHandler(svc, nil, nil, nil, nil, nil, nil)

body := map[string]any{
"promo_code_enabled": true,
"openai_images_responses_reasoning_effort": "xhigh",
}
rawBody, err := json.Marshal(body)
require.NoError(t, err)

rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings", bytes.NewReader(rawBody))
c.Request.Header.Set("Content-Type", "application/json")

handler.UpdateSettings(c)

require.Equal(t, http.StatusOK, rec.Code)
require.Equal(t, service.OpenAIImagesResponsesReasoningEffortXHigh, repo.values[service.SettingKeyOpenAIImagesResponsesReasoningEffort])

var resp response.Response
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
data, ok := resp.Data.(map[string]any)
require.True(t, ok)
require.Equal(t, service.OpenAIImagesResponsesReasoningEffortXHigh, data["openai_images_responses_reasoning_effort"])
}

func TestSettingHandler_UpdateSettings_RejectsInvalidOpenAIImagesResponsesReasoningEffort(t *testing.T) {
gin.SetMode(gin.TestMode)
repo := &settingHandlerRepoStub{
values: map[string]string{
service.SettingKeyPromoCodeEnabled: "true",
},
}
svc := service.NewSettingService(repo, &config.Config{Default: config.DefaultConfig{UserConcurrency: 5}})
handler := NewSettingHandler(svc, nil, nil, nil, nil, nil, nil)

body := map[string]any{
"promo_code_enabled": true,
"openai_images_responses_reasoning_effort": "advanced",
}
rawBody, err := json.Marshal(body)
require.NoError(t, err)

rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings", bytes.NewReader(rawBody))
c.Request.Header.Set("Content-Type", "application/json")

handler.UpdateSettings(c)

require.Equal(t, http.StatusBadRequest, rec.Code)
require.NotContains(t, repo.values, service.SettingKeyOpenAIImagesResponsesReasoningEffort)
}

func TestSettingHandler_UpdateSettings_PreservesLegacyBlankPaymentVisibleMethodSource(t *testing.T) {
gin.SetMode(gin.TestMode)
repo := &settingHandlerRepoStub{
Expand Down
Loading
Loading