From a546871a80e09c733f5803972865ecbdb3487684 Mon Sep 17 00:00:00 2001 From: Seefs Date: Sat, 21 Feb 2026 14:25:58 +0800 Subject: [PATCH] feat: gate Claude inference_geo passthrough behind channel setting and add field docs --- dto/channel_settings.go | 1 + dto/claude.go | 8 +-- dto/openai_request.go | 54 +++++++++++-------- relay/common/override_test.go | 33 ++++++++++++ relay/common/relay_info.go | 8 +++ .../channels/modals/EditChannelModal.jsx | 23 ++++++++ 6 files changed, 101 insertions(+), 26 deletions(-) diff --git a/dto/channel_settings.go b/dto/channel_settings.go index 58c15db0b..72fdf460c 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -29,6 +29,7 @@ type ChannelOtherSettings struct { 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 透传(默认过滤以避免关闭流混淆保护) diff --git a/dto/claude.go b/dto/claude.go index bad3835fa..32e31710b 100644 --- a/dto/claude.go +++ b/dto/claude.go @@ -194,8 +194,9 @@ type ClaudeRequest struct { Prompt string `json:"prompt,omitempty"` System any `json:"system,omitempty"` Messages []ClaudeMessage `json:"messages,omitempty"` - // https://platform.claude.com/docs/en/build-with-claude/data-residency#inference-geo - // InferenceGeo string `json:"inference_geo,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"` @@ -212,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"` } diff --git a/dto/openai_request.go b/dto/openai_request.go index 0b261a61e..c0a69a376 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -56,18 +56,20 @@ type GeneralOpenAIRequest struct { ToolChoice any `json:"tool_choice,omitempty"` FunctionCall json.RawMessage `json:"function_call,omitempty"` User string `json:"user,omitempty"` - 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"` + // 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"` @@ -263,7 +265,8 @@ type FunctionRequest struct { type StreamOptions struct { IncludeUsage bool `json:"include_usage,omitempty"` - // for /v1/responses + // 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"` } @@ -817,23 +820,28 @@ type OpenAIResponsesRequest struct { 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"` - 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"` + // 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 diff --git a/relay/common/override_test.go b/relay/common/override_test.go index 4e8cd5cff..c83cddff7 100644 --- a/relay/common/override_test.go +++ b/relay/common/override_test.go @@ -812,6 +812,39 @@ func TestRemoveDisabledFieldsSkipWhenGlobalPassThroughEnabled(t *testing.T) { 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() diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 491bfb67d..c5c5a883f 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -700,6 +700,7 @@ 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 支持,涉及用户隐私) // stream_options.include_obfuscation: 响应流混淆控制字段(仅 OpenAI Responses API 支持) @@ -721,6 +722,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 { diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index a33c070c4..931a42efb 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -171,6 +171,7 @@ const EditChannelModal = (props) => { 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); @@ -637,6 +638,8 @@ const EditChannelModal = (props) => { 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); @@ -649,6 +652,7 @@ const EditChannelModal = (props) => { 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 { @@ -660,6 +664,7 @@ const EditChannelModal = (props) => { data.disable_store = false; data.allow_safety_identifier = false; data.allow_include_obfuscation = false; + data.allow_inference_geo = false; data.claude_beta_query = false; } @@ -1406,6 +1411,7 @@ const EditChannelModal = (props) => { 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; } } @@ -1429,6 +1435,7 @@ const EditChannelModal = (props) => { 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; @@ -3322,6 +3329,22 @@ const EditChannelModal = (props) => { 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', )} /> + + + handleChannelOtherSettingsChange( + 'allow_inference_geo', + value, + ) + } + extraText={t( + 'inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息', + )} + /> )}