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
1 change: 1 addition & 0 deletions constant/context_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
ContextKeyChannelSetting ContextKey = "channel_setting"
ContextKeyChannelOtherSetting ContextKey = "channel_other_setting"
ContextKeyChannelParamOverride ContextKey = "param_override"
ContextKeyChannelHeaderOverride ContextKey = "header_override"
ContextKeyChannelOrganization ContextKey = "channel_organization"
ContextKeyChannelAutoBan ContextKey = "auto_ban"
ContextKeyChannelModelMapping ContextKey = "model_mapping"
Expand Down
1 change: 1 addition & 0 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
common.SetContextKey(c, constant.ContextKeyChannelSetting, channel.GetSetting())
common.SetContextKey(c, constant.ContextKeyChannelOtherSetting, channel.GetOtherSettings())
common.SetContextKey(c, constant.ContextKeyChannelParamOverride, channel.GetParamOverride())
common.SetContextKey(c, constant.ContextKeyChannelHeaderOverride, channel.GetHeaderOverride())
if nil != channel.OpenAIOrganization && *channel.OpenAIOrganization != "" {
common.SetContextKey(c, constant.ContextKeyChannelOrganization, *channel.OpenAIOrganization)
}
Expand Down
12 changes: 12 additions & 0 deletions model/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Channel struct {
Tag *string `json:"tag" gorm:"index"`
Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
ParamOverride *string `json:"param_override" gorm:"type:text"`
HeaderOverride *string `json:"header_override" gorm:"type:text"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Adding HeaderOverride column — ensure DB migration and add field comment

The new GORM field is fine. Please:

  • Add a short comment explaining expected JSON structure (flat key->string map).
  • Verify migration adds header_override to the channels table in all supported DBs.

Run this script to confirm migrations/auto-migrate cover the new column and to locate usages:


🏁 Script executed:

#!/bin/bash
# Find AutoMigrate and any schema changes referencing Channel or header_override
rg -nP 'AutoMigrate\([^)]*Channel' -C2
rg -n --json 'header_override' -C2
rg -nP '(?i)(migrate|migration|schema|alter|add column|auto\s*migrate).*channel' -S -C2

Length of output: 9291


Add DB migration for header_override and document its JSON structure

Please ensure the new HeaderOverride field in model/channel.go is fully supported:

  • Database migration
    No existing migration or AutoMigrate call adds the header_override column to the channels table. Add a migration (or update your auto-migrate logic) for all supported dialects. For example:

    ALTER TABLE channels ADD COLUMN header_override TEXT;

    • If you use a versioned migrations folder (e.g. migrations/), add a new up/down SQL file.
    • If you rely on GORM’s AutoMigrate, locate where AutoMigrate(&Channel{}) is invoked and confirm it’s applied in your deployment flow.

  • Field comment in Go model
    Explain the expected JSON format (flat map of header names to values). For example, above the field in model/channel.go:

     // HeaderOverride holds a JSON-encoded map of HTTP header names to override values.
     // Example: {"User-Agent":"MyApp/1.0","Accept":"application/json"}
     HeaderOverride    *string `json:"header_override" gorm:"type:text"`

Locations to update:

  • model/channel.go (line 49) – add the above Go doc comment.
  • Migration files or auto-migrate invocation – ensure header_override is added to the channels table schema.

// add after v0.8.5
ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`

Expand Down Expand Up @@ -875,6 +876,17 @@ func (channel *Channel) GetParamOverride() map[string]interface{} {
return paramOverride
}

func (channel *Channel) GetHeaderOverride() map[string]interface{} {
headerOverride := make(map[string]interface{})
if channel.HeaderOverride != nil && *channel.HeaderOverride != "" {
err := common.Unmarshal([]byte(*channel.HeaderOverride), &headerOverride)
if err != nil {
common.SysLog(fmt.Sprintf("failed to unmarshal header override: channel_id=%d, error=%v", channel.Id, err))
}
}
return headerOverride
}

func GetChannelsByIds(ids []int) ([]*Channel, error) {
var channels []*Channel
err := DB.Where("id in (?)", ids).Find(&channels).Error
Expand Down
30 changes: 27 additions & 3 deletions relay/channel/api_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"one-api/relay/helper"
"one-api/service"
"one-api/setting/operation_setting"
"one-api/types"
"sync"
"time"

Expand Down Expand Up @@ -47,7 +48,19 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody
if err != nil {
return nil, fmt.Errorf("new request failed: %w", err)
}
err = a.SetupRequestHeader(c, &req.Header, info)
headers := req.Header
headerOverride := make(map[string]string)
for k, v := range info.HeadersOverride {
if str, ok := v.(string); ok {
headerOverride[k] = str
} else {
return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
}
}
for key, value := range headerOverride {
headers.Set(key, value)
}
err = a.SetupRequestHeader(c, &headers, info)
Comment on lines +51 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Header overrides may be clobbered by SetupRequestHeader; also passing a nil err into types.NewError

  • Order issue: You set overrides, then call a.SetupRequestHeader, which (via SetupApiRequestHeader) resets Content-Type/Accept (and many adaptors may set Authorization). This can undo the override. Expected behavior is “override wins,” so apply overrides after SetupRequestHeader.
  • Error issue: On non-string override values you call types.NewError(err, ...), but err is nil here, losing the root cause and stack. Pass a constructed error, ideally with a 400 status.

Apply this diff:

-headers := req.Header
-headerOverride := make(map[string]string)
-for k, v := range info.HeadersOverride {
-  if str, ok := v.(string); ok {
-    headerOverride[k] = str
-  } else {
-    return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
-  }
-}
-for key, value := range headerOverride {
-  headers.Set(key, value)
-}
-err = a.SetupRequestHeader(c, &headers, info)
+headers := req.Header
+// First, let adaptor set its defaults (auth, content-type, etc.).
+if err = a.SetupRequestHeader(c, &headers, info); err != nil {
+  return nil, fmt.Errorf("setup request header failed: %w", err)
+}
+// Then apply channel overrides so they take precedence.
+for k, v := range info.HeadersOverride {
+  s, ok := v.(string)
+  if !ok {
+    return nil, types.NewError(
+      fmt.Errorf("header_override %q must be string, got %T", k, v),
+      types.ErrorCodeChannelHeaderOverrideInvalid,
+    )
+  }
+  headers.Set(k, s)
+}

Optional hardening: filter disallowed headers (Host, Content-Length, Transfer-Encoding, Connection, TE, Trailer, Upgrade, Expect) or CR/LF in values before Set.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
headers := req.Header
headerOverride := make(map[string]string)
for k, v := range info.HeadersOverride {
if str, ok := v.(string); ok {
headerOverride[k] = str
} else {
return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
}
}
for key, value := range headerOverride {
headers.Set(key, value)
}
err = a.SetupRequestHeader(c, &headers, info)
headers := req.Header
// First, let adaptor set its defaults (auth, content-type, etc.).
if err = a.SetupRequestHeader(c, &headers, info); err != nil {
return nil, fmt.Errorf("setup request header failed: %w", err)
}
// Then apply channel overrides so they take precedence.
for k, v := range info.HeadersOverride {
s, ok := v.(string)
if !ok {
return nil, types.NewError(
fmt.Errorf("header_override %q must be string, got %T", k, v),
types.ErrorCodeChannelHeaderOverrideInvalid,
)
}
headers.Set(k, s)
}
🤖 Prompt for AI Agents
In relay/channel/api_request.go around lines 51-63, the header override handling
is done before calling a.SetupRequestHeader which may reset or overwrite those
values and the code also calls types.NewError(err, ...) with err == nil on
non-string values. Fix by first calling a.SetupRequestHeader(c, &req.Header,
info) to let adaptors set defaults, then apply info.HeadersOverride (casting
each value to string and returning types.NewError(errors.New("invalid header
override value"), types.ErrorCodeChannelHeaderOverrideInvalid) or a similar
400-level error when a value is not a string). When setting headers, skip
disallowed header names (Host, Content-Length, Transfer-Encoding, Connection,
TE, Trailer, Upgrade, Expect) and validate values to reject CR/LF before calling
headers.Set.

if err != nil {
return nil, fmt.Errorf("setup request header failed: %w", err)
}
Expand All @@ -72,8 +85,19 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod
}
// set form data
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))

err = a.SetupRequestHeader(c, &req.Header, info)
headers := req.Header
headerOverride := make(map[string]string)
for k, v := range info.HeadersOverride {
if str, ok := v.(string); ok {
headerOverride[k] = str
} else {
return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
}
}
for key, value := range headerOverride {
headers.Set(key, value)
}
err = a.SetupRequestHeader(c, &headers, info)
if err != nil {
Comment on lines 86 to 101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same two issues in DoFormRequest: override order and nil err passed to NewError

Replicate the fix used for DoApiRequest so that:

  • Adaptor defaults are set first.
  • Override headers take precedence and invalid entries return a clear 400-style error.

Apply this diff:

-// set form data
-req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
-headers := req.Header
-headerOverride := make(map[string]string)
-for k, v := range info.HeadersOverride {
-  if str, ok := v.(string); ok {
-    headerOverride[k] = str
-  } else {
-    return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
-  }
-}
-for key, value := range headerOverride {
-  headers.Set(key, value)
-}
-err = a.SetupRequestHeader(c, &headers, info)
+// Preserve incoming content-type for form uploads
+req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
+headers := req.Header
+// First set adaptor defaults
+if err = a.SetupRequestHeader(c, &headers, info); err != nil {
+  return nil, fmt.Errorf("setup request header failed: %w", err)
+}
+// Then apply overrides so they win
+for k, v := range info.HeadersOverride {
+  s, ok := v.(string)
+  if !ok {
+    return nil, types.NewError(
+      fmt.Errorf("header_override %q must be string, got %T", k, v),
+      types.ErrorCodeChannelHeaderOverrideInvalid,
+    )
+  }
+  headers.Set(k, s)
+}

If you add disallowed header filtering in DoApiRequest, mirror it here for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// set form data
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
err = a.SetupRequestHeader(c, &req.Header, info)
headers := req.Header
headerOverride := make(map[string]string)
for k, v := range info.HeadersOverride {
if str, ok := v.(string); ok {
headerOverride[k] = str
} else {
return nil, types.NewError(err, types.ErrorCodeChannelHeaderOverrideInvalid)
}
}
for key, value := range headerOverride {
headers.Set(key, value)
}
err = a.SetupRequestHeader(c, &headers, info)
if err != nil {
// Preserve incoming content-type for form uploads
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
headers := req.Header
// First set adaptor defaults
if err = a.SetupRequestHeader(c, &headers, info); err != nil {
return nil, fmt.Errorf("setup request header failed: %w", err)
}
// Then apply overrides so they win
for k, v := range info.HeadersOverride {
s, ok := v.(string)
if !ok {
return nil, types.NewError(
fmt.Errorf("header_override %q must be string, got %T", k, v),
types.ErrorCodeChannelHeaderOverrideInvalid,
)
}
headers.Set(k, s)
}
🤖 Prompt for AI Agents
In relay/channel/api_request.go around lines 86 to 101, DoFormRequest currently
applies adaptor defaults after applying header overrides and calls
types.NewError with a nil err; change the order so adaptor default headers are
set first (call a.SetupRequestHeader or equivalent to populate base headers),
then apply header overrides so they take precedence, validate each override
entry and return types.NewError(errors.New("invalid header override"),
types.ErrorCodeChannelHeaderOverrideInvalid) when a non-string value is found
(do not pass nil), and if you added disallowed header filtering in DoApiRequest,
apply the same filtering here so disallowed headers are removed/blocked
consistently before sending the request.

return nil, fmt.Errorf("setup request header failed: %w", err)
}
Expand Down
3 changes: 3 additions & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type ChannelMeta struct {
Organization string
ChannelCreateTime int64
ParamOverride map[string]interface{}
HeadersOverride map[string]interface{}
ChannelSetting dto.ChannelSettings
ChannelOtherSettings dto.ChannelOtherSettings
UpstreamModelName string
Expand Down Expand Up @@ -120,6 +121,7 @@ type RelayInfo struct {
func (info *RelayInfo) InitChannelMeta(c *gin.Context) {
channelType := common.GetContextKeyInt(c, constant.ContextKeyChannelType)
paramOverride := common.GetContextKeyStringMap(c, constant.ContextKeyChannelParamOverride)
headerOverride := common.GetContextKeyStringMap(c, constant.ContextKeyChannelHeaderOverride)
apiType, _ := common.ChannelType2APIType(channelType)
channelMeta := &ChannelMeta{
ChannelType: channelType,
Expand All @@ -133,6 +135,7 @@ func (info *RelayInfo) InitChannelMeta(c *gin.Context) {
Organization: c.GetString("channel_organization"),
ChannelCreateTime: c.GetInt64("channel_create_time"),
ParamOverride: paramOverride,
HeadersOverride: headerOverride,
UpstreamModelName: common.GetContextKeyString(c, constant.ContextKeyOriginalModel),
IsModelMapped: false,
SupportStreamOptions: false,
Expand Down
13 changes: 7 additions & 6 deletions types/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ const (
ErrorCodeGenRelayInfoFailed ErrorCode = "gen_relay_info_failed"

// channel error
ErrorCodeChannelNoAvailableKey ErrorCode = "channel:no_available_key"
ErrorCodeChannelParamOverrideInvalid ErrorCode = "channel:param_override_invalid"
ErrorCodeChannelModelMappedError ErrorCode = "channel:model_mapped_error"
ErrorCodeChannelAwsClientError ErrorCode = "channel:aws_client_error"
ErrorCodeChannelInvalidKey ErrorCode = "channel:invalid_key"
ErrorCodeChannelResponseTimeExceeded ErrorCode = "channel:response_time_exceeded"
ErrorCodeChannelNoAvailableKey ErrorCode = "channel:no_available_key"
ErrorCodeChannelParamOverrideInvalid ErrorCode = "channel:param_override_invalid"
ErrorCodeChannelHeaderOverrideInvalid ErrorCode = "channel:header_override_invalid"
ErrorCodeChannelModelMappedError ErrorCode = "channel:model_mapped_error"
ErrorCodeChannelAwsClientError ErrorCode = "channel:aws_client_error"
ErrorCodeChannelInvalidKey ErrorCode = "channel:invalid_key"
ErrorCodeChannelResponseTimeExceeded ErrorCode = "channel:response_time_exceeded"

// client request error
ErrorCodeReadRequestBodyFailed ErrorCode = "read_request_body_failed"
Expand Down
25 changes: 25 additions & 0 deletions web/src/components/table/channels/modals/EditChannelModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,31 @@ const EditChannelModal = (props) => {
showClear
/>

<Form.TextArea
field='header_override'
label={t('请求头覆盖')}
placeholder={
t('此项可选,用于覆盖请求头参数') +
'\n' + t('格式示例:') +
'\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0"\n}'
}
autosize
onChange={(value) => handleInputChange('header_override', value)}
extraText={
<div className="flex gap-2 flex-wrap">
<Text
className="!text-semi-color-primary cursor-pointer"
onClick={() => handleInputChange('header_override', JSON.stringify({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0"
}, null, 2))}
>
{t('格式模板')}
</Text>
</div>
}
showClear
/>


<JSONEditor
key={`status_code_mapping-${isEdit ? channelId : 'new'}`}
Expand Down