mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 03:23:59 +00:00
feat: add missing OpenAI/Claude/Gemini request fields (#2971)
* feat: add missing OpenAI/Claude/Gemini request fields and responses stream options * fix: skip field filtering when request passthrough is enabled * fix: include subscription in personal sidebar module controls * feat: gate Claude inference_geo passthrough behind channel setting and add field docs
This commit is contained in:
@@ -24,14 +24,16 @@ const (
|
||||
)
|
||||
|
||||
type ChannelOtherSettings struct {
|
||||
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
|
||||
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
|
||||
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
|
||||
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
|
||||
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
|
||||
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
|
||||
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
|
||||
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
|
||||
AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
|
||||
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
|
||||
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
|
||||
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
|
||||
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
|
||||
AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude,默认过滤以满足数据驻留合规)
|
||||
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
|
||||
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
|
||||
AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
|
||||
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
|
||||
}
|
||||
|
||||
func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
|
||||
|
||||
@@ -190,10 +190,13 @@ type ClaudeToolChoice struct {
|
||||
}
|
||||
|
||||
type ClaudeRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
System any `json:"system,omitempty"`
|
||||
Messages []ClaudeMessage `json:"messages,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
System any `json:"system,omitempty"`
|
||||
Messages []ClaudeMessage `json:"messages,omitempty"`
|
||||
// InferenceGeo controls Claude data residency region.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_inference_geo.
|
||||
InferenceGeo string `json:"inference_geo,omitempty"`
|
||||
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||
MaxTokensToSample uint `json:"max_tokens_to_sample,omitempty"`
|
||||
StopSequences []string `json:"stop_sequences,omitempty"`
|
||||
@@ -210,7 +213,8 @@ type ClaudeRequest struct {
|
||||
Thinking *Thinking `json:"thinking,omitempty"`
|
||||
McpServers json.RawMessage `json:"mcp_servers,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
// 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
|
||||
// ServiceTier specifies upstream service level and may affect billing.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_service_tier.
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -324,25 +324,26 @@ type GeminiChatTool struct {
|
||||
}
|
||||
|
||||
type GeminiChatGenerationConfig struct {
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
TopK float64 `json:"topK,omitempty"`
|
||||
MaxOutputTokens uint `json:"maxOutputTokens,omitempty"`
|
||||
CandidateCount int `json:"candidateCount,omitempty"`
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
ResponseMimeType string `json:"responseMimeType,omitempty"`
|
||||
ResponseSchema any `json:"responseSchema,omitempty"`
|
||||
ResponseJsonSchema json.RawMessage `json:"responseJsonSchema,omitempty"`
|
||||
PresencePenalty *float32 `json:"presencePenalty,omitempty"`
|
||||
FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty"`
|
||||
ResponseLogprobs bool `json:"responseLogprobs,omitempty"`
|
||||
Logprobs *int32 `json:"logprobs,omitempty"`
|
||||
MediaResolution MediaResolution `json:"mediaResolution,omitempty"`
|
||||
Seed int64 `json:"seed,omitempty"`
|
||||
ResponseModalities []string `json:"responseModalities,omitempty"`
|
||||
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
|
||||
SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config
|
||||
ImageConfig json.RawMessage `json:"imageConfig,omitempty"` // RawMessage to allow flexible image config
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
TopK float64 `json:"topK,omitempty"`
|
||||
MaxOutputTokens uint `json:"maxOutputTokens,omitempty"`
|
||||
CandidateCount int `json:"candidateCount,omitempty"`
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
ResponseMimeType string `json:"responseMimeType,omitempty"`
|
||||
ResponseSchema any `json:"responseSchema,omitempty"`
|
||||
ResponseJsonSchema json.RawMessage `json:"responseJsonSchema,omitempty"`
|
||||
PresencePenalty *float32 `json:"presencePenalty,omitempty"`
|
||||
FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty"`
|
||||
ResponseLogprobs bool `json:"responseLogprobs,omitempty"`
|
||||
Logprobs *int32 `json:"logprobs,omitempty"`
|
||||
EnableEnhancedCivicAnswers *bool `json:"enableEnhancedCivicAnswers,omitempty"`
|
||||
MediaResolution MediaResolution `json:"mediaResolution,omitempty"`
|
||||
Seed int64 `json:"seed,omitempty"`
|
||||
ResponseModalities []string `json:"responseModalities,omitempty"`
|
||||
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
|
||||
SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config
|
||||
ImageConfig json.RawMessage `json:"imageConfig,omitempty"` // RawMessage to allow flexible image config
|
||||
}
|
||||
|
||||
// UnmarshalJSON allows GeminiChatGenerationConfig to accept both snake_case and camelCase fields.
|
||||
@@ -350,22 +351,23 @@ func (c *GeminiChatGenerationConfig) UnmarshalJSON(data []byte) error {
|
||||
type Alias GeminiChatGenerationConfig
|
||||
var aux struct {
|
||||
Alias
|
||||
TopPSnake float64 `json:"top_p,omitempty"`
|
||||
TopKSnake float64 `json:"top_k,omitempty"`
|
||||
MaxOutputTokensSnake uint `json:"max_output_tokens,omitempty"`
|
||||
CandidateCountSnake int `json:"candidate_count,omitempty"`
|
||||
StopSequencesSnake []string `json:"stop_sequences,omitempty"`
|
||||
ResponseMimeTypeSnake string `json:"response_mime_type,omitempty"`
|
||||
ResponseSchemaSnake any `json:"response_schema,omitempty"`
|
||||
ResponseJsonSchemaSnake json.RawMessage `json:"response_json_schema,omitempty"`
|
||||
PresencePenaltySnake *float32 `json:"presence_penalty,omitempty"`
|
||||
FrequencyPenaltySnake *float32 `json:"frequency_penalty,omitempty"`
|
||||
ResponseLogprobsSnake bool `json:"response_logprobs,omitempty"`
|
||||
MediaResolutionSnake MediaResolution `json:"media_resolution,omitempty"`
|
||||
ResponseModalitiesSnake []string `json:"response_modalities,omitempty"`
|
||||
ThinkingConfigSnake *GeminiThinkingConfig `json:"thinking_config,omitempty"`
|
||||
SpeechConfigSnake json.RawMessage `json:"speech_config,omitempty"`
|
||||
ImageConfigSnake json.RawMessage `json:"image_config,omitempty"`
|
||||
TopPSnake float64 `json:"top_p,omitempty"`
|
||||
TopKSnake float64 `json:"top_k,omitempty"`
|
||||
MaxOutputTokensSnake uint `json:"max_output_tokens,omitempty"`
|
||||
CandidateCountSnake int `json:"candidate_count,omitempty"`
|
||||
StopSequencesSnake []string `json:"stop_sequences,omitempty"`
|
||||
ResponseMimeTypeSnake string `json:"response_mime_type,omitempty"`
|
||||
ResponseSchemaSnake any `json:"response_schema,omitempty"`
|
||||
ResponseJsonSchemaSnake json.RawMessage `json:"response_json_schema,omitempty"`
|
||||
PresencePenaltySnake *float32 `json:"presence_penalty,omitempty"`
|
||||
FrequencyPenaltySnake *float32 `json:"frequency_penalty,omitempty"`
|
||||
ResponseLogprobsSnake bool `json:"response_logprobs,omitempty"`
|
||||
EnableEnhancedCivicAnswersSnake *bool `json:"enable_enhanced_civic_answers,omitempty"`
|
||||
MediaResolutionSnake MediaResolution `json:"media_resolution,omitempty"`
|
||||
ResponseModalitiesSnake []string `json:"response_modalities,omitempty"`
|
||||
ThinkingConfigSnake *GeminiThinkingConfig `json:"thinking_config,omitempty"`
|
||||
SpeechConfigSnake json.RawMessage `json:"speech_config,omitempty"`
|
||||
ImageConfigSnake json.RawMessage `json:"image_config,omitempty"`
|
||||
}
|
||||
|
||||
if err := common.Unmarshal(data, &aux); err != nil {
|
||||
@@ -408,6 +410,9 @@ func (c *GeminiChatGenerationConfig) UnmarshalJSON(data []byte) error {
|
||||
if aux.ResponseLogprobsSnake {
|
||||
c.ResponseLogprobs = aux.ResponseLogprobsSnake
|
||||
}
|
||||
if aux.EnableEnhancedCivicAnswersSnake != nil {
|
||||
c.EnableEnhancedCivicAnswers = aux.EnableEnhancedCivicAnswersSnake
|
||||
}
|
||||
if aux.MediaResolutionSnake != "" {
|
||||
c.MediaResolution = aux.MediaResolutionSnake
|
||||
}
|
||||
|
||||
@@ -54,18 +54,22 @@ type GeneralOpenAIRequest struct {
|
||||
ParallelTooCalls *bool `json:"parallel_tool_calls,omitempty"`
|
||||
Tools []ToolCallRequest `json:"tools,omitempty"`
|
||||
ToolChoice any `json:"tool_choice,omitempty"`
|
||||
FunctionCall json.RawMessage `json:"function_call,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
LogProbs bool `json:"logprobs,omitempty"`
|
||||
TopLogProbs int `json:"top_logprobs,omitempty"`
|
||||
Dimensions int `json:"dimensions,omitempty"`
|
||||
Modalities json.RawMessage `json:"modalities,omitempty"`
|
||||
Audio json.RawMessage `json:"audio,omitempty"`
|
||||
// ServiceTier specifies upstream service level and may affect billing.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_service_tier.
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
LogProbs bool `json:"logprobs,omitempty"`
|
||||
TopLogProbs int `json:"top_logprobs,omitempty"`
|
||||
Dimensions int `json:"dimensions,omitempty"`
|
||||
Modalities json.RawMessage `json:"modalities,omitempty"`
|
||||
Audio json.RawMessage `json:"audio,omitempty"`
|
||||
// 安全标识符,用于帮助 OpenAI 检测可能违反使用政策的应用程序用户
|
||||
// 注意:此字段会向 OpenAI 发送用户标识信息,默认过滤以保护用户隐私
|
||||
// 注意:此字段会向 OpenAI 发送用户标识信息,默认过滤,可通过 allow_safety_identifier 开启
|
||||
SafetyIdentifier string `json:"safety_identifier,omitempty"`
|
||||
// Whether or not to store the output of this chat completion request for use in our model distillation or evals products.
|
||||
// 是否存储此次请求数据供 OpenAI 用于评估和优化产品
|
||||
// 注意:默认过滤此字段以保护用户隐私,但过滤后可能导致 Codex 无法正常使用
|
||||
// 注意:默认允许透传,可通过 disable_store 禁用;禁用后可能导致 Codex 无法正常使用
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field
|
||||
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
|
||||
@@ -261,6 +265,9 @@ type FunctionRequest struct {
|
||||
|
||||
type StreamOptions struct {
|
||||
IncludeUsage bool `json:"include_usage,omitempty"`
|
||||
// IncludeObfuscation is only for /v1/responses stream payload.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_include_obfuscation.
|
||||
IncludeObfuscation bool `json:"include_obfuscation,omitempty"`
|
||||
}
|
||||
|
||||
func (r *GeneralOpenAIRequest) GetMaxTokens() uint {
|
||||
@@ -799,30 +806,42 @@ type WebSearchOptions struct {
|
||||
|
||||
// https://platform.openai.com/docs/api-reference/responses/create
|
||||
type OpenAIResponsesRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
Include json.RawMessage `json:"include,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
Include json.RawMessage `json:"include,omitempty"`
|
||||
// 在后台运行推理,暂时还不支持依赖的接口
|
||||
// Background json.RawMessage `json:"background,omitempty"`
|
||||
Conversation json.RawMessage `json:"conversation,omitempty"`
|
||||
ContextManagement json.RawMessage `json:"context_management,omitempty"`
|
||||
Instructions json.RawMessage `json:"instructions,omitempty"`
|
||||
MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
|
||||
TopLogProbs *int `json:"top_logprobs,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"`
|
||||
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
||||
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
||||
// 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
// ServiceTier specifies upstream service level and may affect billing.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_service_tier.
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
// Store controls whether upstream may store request/response data.
|
||||
// This field is allowed by default and can be disabled via channel setting disable_store.
|
||||
Store json.RawMessage `json:"store,omitempty"`
|
||||
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||
PromptCacheRetention json.RawMessage `json:"prompt_cache_retention,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Text json.RawMessage `json:"text,omitempty"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||
Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
|
||||
TopP *float64 `json:"top_p,omitempty"`
|
||||
Truncation string `json:"truncation,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
MaxToolCalls uint `json:"max_tool_calls,omitempty"`
|
||||
Prompt json.RawMessage `json:"prompt,omitempty"`
|
||||
// SafetyIdentifier carries client identity for policy abuse detection.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_safety_identifier.
|
||||
SafetyIdentifier string `json:"safety_identifier,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
Text json.RawMessage `json:"text,omitempty"`
|
||||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||||
Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
|
||||
TopP *float64 `json:"top_p,omitempty"`
|
||||
Truncation string `json:"truncation,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
MaxToolCalls uint `json:"max_tool_calls,omitempty"`
|
||||
Prompt json.RawMessage `json:"prompt,omitempty"`
|
||||
// qwen
|
||||
EnableThinking json.RawMessage `json:"enable_thinking,omitempty"`
|
||||
// perplexity
|
||||
|
||||
@@ -76,7 +76,7 @@ func chatCompletionsViaResponses(c *gin.Context, info *relaycommon.RelayInfo, ad
|
||||
return nil, types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
chatJSON, err = relaycommon.RemoveDisabledFields(chatJSON, info.ChannelOtherSettings)
|
||||
chatJSON, err = relaycommon.RemoveDisabledFields(chatJSON, info.ChannelOtherSettings, info.ChannelSetting.PassThroughBodyEnabled)
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func chatCompletionsViaResponses(c *gin.Context, info *relaycommon.RelayInfo, ad
|
||||
return nil, types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
|
||||
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings, info.ChannelSetting.PassThroughBodyEnabled)
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
|
||||
}
|
||||
|
||||
// remove disabled fields for Claude API
|
||||
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
|
||||
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings, info.ChannelSetting.PassThroughBodyEnabled)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/setting/model_setting"
|
||||
)
|
||||
|
||||
func TestApplyParamOverrideTrimPrefix(t *testing.T) {
|
||||
@@ -772,6 +775,76 @@ func TestApplyParamOverrideToUpper(t *testing.T) {
|
||||
assertJSONEqual(t, `{"model":"GPT-4"}`, string(out))
|
||||
}
|
||||
|
||||
func TestRemoveDisabledFieldsSkipWhenChannelPassThroughEnabled(t *testing.T) {
|
||||
input := `{
|
||||
"service_tier":"flex",
|
||||
"safety_identifier":"user-123",
|
||||
"store":true,
|
||||
"stream_options":{"include_obfuscation":false}
|
||||
}`
|
||||
settings := dto.ChannelOtherSettings{}
|
||||
|
||||
out, err := RemoveDisabledFields([]byte(input), settings, true)
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveDisabledFields returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, input, string(out))
|
||||
}
|
||||
|
||||
func TestRemoveDisabledFieldsSkipWhenGlobalPassThroughEnabled(t *testing.T) {
|
||||
original := model_setting.GetGlobalSettings().PassThroughRequestEnabled
|
||||
model_setting.GetGlobalSettings().PassThroughRequestEnabled = true
|
||||
t.Cleanup(func() {
|
||||
model_setting.GetGlobalSettings().PassThroughRequestEnabled = original
|
||||
})
|
||||
|
||||
input := `{
|
||||
"service_tier":"flex",
|
||||
"safety_identifier":"user-123",
|
||||
"stream_options":{"include_obfuscation":false}
|
||||
}`
|
||||
settings := dto.ChannelOtherSettings{}
|
||||
|
||||
out, err := RemoveDisabledFields([]byte(input), settings, false)
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveDisabledFields returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, input, string(out))
|
||||
}
|
||||
|
||||
func TestRemoveDisabledFieldsDefaultFiltering(t *testing.T) {
|
||||
input := `{
|
||||
"service_tier":"flex",
|
||||
"inference_geo":"eu",
|
||||
"safety_identifier":"user-123",
|
||||
"store":true,
|
||||
"stream_options":{"include_obfuscation":false}
|
||||
}`
|
||||
settings := dto.ChannelOtherSettings{}
|
||||
|
||||
out, err := RemoveDisabledFields([]byte(input), settings, false)
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveDisabledFields returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"store":true}`, string(out))
|
||||
}
|
||||
|
||||
func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) {
|
||||
input := `{
|
||||
"inference_geo":"eu",
|
||||
"store":true
|
||||
}`
|
||||
settings := dto.ChannelOtherSettings{
|
||||
AllowInferenceGeo: true,
|
||||
}
|
||||
|
||||
out, err := RemoveDisabledFields([]byte(input), settings, false)
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveDisabledFields returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out))
|
||||
}
|
||||
|
||||
func assertJSONEqual(t *testing.T, want, got string) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -728,9 +728,15 @@ func FailTaskInfo(reason string) *TaskInfo {
|
||||
|
||||
// RemoveDisabledFields 从请求 JSON 数据中移除渠道设置中禁用的字段
|
||||
// service_tier: 服务层级字段,可能导致额外计费(OpenAI、Claude、Responses API 支持)
|
||||
// inference_geo: Claude 数据驻留推理区域字段(仅 Claude 支持,默认过滤)
|
||||
// store: 数据存储授权字段,涉及用户隐私(仅 OpenAI、Responses API 支持,默认允许透传,禁用后可能导致 Codex 无法使用)
|
||||
// safety_identifier: 安全标识符,用于向 OpenAI 报告违规用户(仅 OpenAI 支持,涉及用户隐私)
|
||||
func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOtherSettings) ([]byte, error) {
|
||||
// stream_options.include_obfuscation: 响应流混淆控制字段(仅 OpenAI Responses API 支持)
|
||||
func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOtherSettings, channelPassThroughEnabled bool) ([]byte, error) {
|
||||
if model_setting.GetGlobalSettings().PassThroughRequestEnabled || channelPassThroughEnabled {
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
var data map[string]interface{}
|
||||
if err := common.Unmarshal(jsonData, &data); err != nil {
|
||||
common.SysError("RemoveDisabledFields Unmarshal error :" + err.Error())
|
||||
@@ -744,6 +750,13 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther
|
||||
}
|
||||
}
|
||||
|
||||
// 默认移除 inference_geo,除非明确允许(避免在未授权情况下透传数据驻留区域)
|
||||
if !channelOtherSettings.AllowInferenceGeo {
|
||||
if _, exists := data["inference_geo"]; exists {
|
||||
delete(data, "inference_geo")
|
||||
}
|
||||
}
|
||||
|
||||
// 默认允许 store 透传,除非明确禁用(禁用可能影响 Codex 使用)
|
||||
if channelOtherSettings.DisableStore {
|
||||
if _, exists := data["store"]; exists {
|
||||
@@ -758,6 +771,22 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther
|
||||
}
|
||||
}
|
||||
|
||||
// 默认移除 stream_options.include_obfuscation,除非明确允许(避免关闭响应流混淆保护)
|
||||
if !channelOtherSettings.AllowIncludeObfuscation {
|
||||
if streamOptionsAny, exists := data["stream_options"]; exists {
|
||||
if streamOptions, ok := streamOptionsAny.(map[string]interface{}); ok {
|
||||
if _, includeExists := streamOptions["include_obfuscation"]; includeExists {
|
||||
delete(streamOptions, "include_obfuscation")
|
||||
}
|
||||
if len(streamOptions) == 0 {
|
||||
delete(data, "stream_options")
|
||||
} else {
|
||||
data["stream_options"] = streamOptions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jsonDataAfter, err := common.Marshal(data)
|
||||
if err != nil {
|
||||
common.SysError("RemoveDisabledFields Marshal error :" + err.Error())
|
||||
|
||||
@@ -165,7 +165,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
|
||||
}
|
||||
|
||||
// remove disabled fields for OpenAI API
|
||||
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
|
||||
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings, info.ChannelSetting.PassThroughBodyEnabled)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
|
||||
}
|
||||
|
||||
// remove disabled fields for OpenAI Responses API
|
||||
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
|
||||
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings, info.ChannelSetting.PassThroughBodyEnabled)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ const NotificationSettings = ({
|
||||
channel: true,
|
||||
models: true,
|
||||
deployment: true,
|
||||
subscription: true,
|
||||
redemption: true,
|
||||
user: true,
|
||||
setting: true,
|
||||
@@ -169,6 +170,7 @@ const NotificationSettings = ({
|
||||
channel: true,
|
||||
models: true,
|
||||
deployment: true,
|
||||
subscription: true,
|
||||
redemption: true,
|
||||
user: true,
|
||||
setting: true,
|
||||
@@ -296,6 +298,11 @@ const NotificationSettings = ({
|
||||
title: t('模型部署'),
|
||||
description: t('模型部署管理'),
|
||||
},
|
||||
{
|
||||
key: 'subscription',
|
||||
title: t('订阅管理'),
|
||||
description: t('订阅套餐管理'),
|
||||
},
|
||||
{
|
||||
key: 'redemption',
|
||||
title: t('兑换码管理'),
|
||||
|
||||
@@ -175,6 +175,8 @@ const EditChannelModal = (props) => {
|
||||
allow_service_tier: false,
|
||||
disable_store: false, // false = 允许透传(默认开启)
|
||||
allow_safety_identifier: false,
|
||||
allow_include_obfuscation: false,
|
||||
allow_inference_geo: false,
|
||||
claude_beta_query: false,
|
||||
};
|
||||
const [batch, setBatch] = useState(false);
|
||||
@@ -646,6 +648,10 @@ const EditChannelModal = (props) => {
|
||||
data.disable_store = parsedSettings.disable_store || false;
|
||||
data.allow_safety_identifier =
|
||||
parsedSettings.allow_safety_identifier || false;
|
||||
data.allow_include_obfuscation =
|
||||
parsedSettings.allow_include_obfuscation || false;
|
||||
data.allow_inference_geo =
|
||||
parsedSettings.allow_inference_geo || false;
|
||||
data.claude_beta_query = parsedSettings.claude_beta_query || false;
|
||||
} catch (error) {
|
||||
console.error('解析其他设置失败:', error);
|
||||
@@ -657,6 +663,8 @@ const EditChannelModal = (props) => {
|
||||
data.allow_service_tier = false;
|
||||
data.disable_store = false;
|
||||
data.allow_safety_identifier = false;
|
||||
data.allow_include_obfuscation = false;
|
||||
data.allow_inference_geo = false;
|
||||
data.claude_beta_query = false;
|
||||
}
|
||||
} else {
|
||||
@@ -667,6 +675,8 @@ const EditChannelModal = (props) => {
|
||||
data.allow_service_tier = false;
|
||||
data.disable_store = false;
|
||||
data.allow_safety_identifier = false;
|
||||
data.allow_include_obfuscation = false;
|
||||
data.allow_inference_geo = false;
|
||||
data.claude_beta_query = false;
|
||||
}
|
||||
|
||||
@@ -1453,13 +1463,16 @@ const EditChannelModal = (props) => {
|
||||
// type === 1 (OpenAI) 或 type === 14 (Claude): 设置字段透传控制(显式保存布尔值)
|
||||
if (localInputs.type === 1 || localInputs.type === 14) {
|
||||
settings.allow_service_tier = localInputs.allow_service_tier === true;
|
||||
// 仅 OpenAI 渠道需要 store 和 safety_identifier
|
||||
// 仅 OpenAI 渠道需要 store / safety_identifier / include_obfuscation
|
||||
if (localInputs.type === 1) {
|
||||
settings.disable_store = localInputs.disable_store === true;
|
||||
settings.allow_safety_identifier =
|
||||
localInputs.allow_safety_identifier === true;
|
||||
settings.allow_include_obfuscation =
|
||||
localInputs.allow_include_obfuscation === true;
|
||||
}
|
||||
if (localInputs.type === 14) {
|
||||
settings.allow_inference_geo = localInputs.allow_inference_geo === true;
|
||||
settings.claude_beta_query = localInputs.claude_beta_query === true;
|
||||
}
|
||||
}
|
||||
@@ -1482,6 +1495,8 @@ const EditChannelModal = (props) => {
|
||||
delete localInputs.allow_service_tier;
|
||||
delete localInputs.disable_store;
|
||||
delete localInputs.allow_safety_identifier;
|
||||
delete localInputs.allow_include_obfuscation;
|
||||
delete localInputs.allow_inference_geo;
|
||||
delete localInputs.claude_beta_query;
|
||||
|
||||
let res;
|
||||
@@ -3332,6 +3347,24 @@ const EditChannelModal = (props) => {
|
||||
'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私',
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Switch
|
||||
field='allow_include_obfuscation'
|
||||
label={t(
|
||||
'允许 stream_options.include_obfuscation 透传',
|
||||
)}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
onChange={(value) =>
|
||||
handleChannelOtherSettingsChange(
|
||||
'allow_include_obfuscation',
|
||||
value,
|
||||
)
|
||||
}
|
||||
extraText={t(
|
||||
'include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3357,6 +3390,22 @@ const EditChannelModal = (props) => {
|
||||
'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用',
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Switch
|
||||
field='allow_inference_geo'
|
||||
label={t('允许 inference_geo 透传')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
onChange={(value) =>
|
||||
handleChannelOtherSettingsChange(
|
||||
'allow_inference_geo',
|
||||
value,
|
||||
)
|
||||
}
|
||||
extraText={t(
|
||||
'inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user